add initial project structure with configuration, models, and API key authentication

This commit is contained in:
Flavio Fois
2026-03-17 12:21:48 +01:00
commit 08ff1da469
16 changed files with 379 additions and 0 deletions

34
internal/config/config.go Normal file
View File

@@ -0,0 +1,34 @@
package config
import (
"os"
"strings"
)
type Config struct {
Port string
DSN string
APIKeys []string
}
func Load() *Config {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
raw := os.Getenv("API_KEYS")
var keys []string
for _, k := range strings.Split(raw, ",") {
k = strings.TrimSpace(k)
if k != "" {
keys = append(keys, k)
}
}
return &Config{
Port: port,
DSN: os.Getenv("DB_DSN"),
APIKeys: keys,
}
}

View File

@@ -0,0 +1,23 @@
package database
import (
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"emly-api-go/internal/config"
)
func Connect(cfg *config.Config) (*sqlx.DB, error) {
db, err := sqlx.Connect("mysql", cfg.DSN)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
return db, nil
}

View File

@@ -0,0 +1,24 @@
package handlers
import (
"encoding/json"
"io"
"net/http"
)
var ExampleGet http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "example GET"})
}
var ExamplePost http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
"message": "example POST",
"received": string(body),
})
}

View File

@@ -0,0 +1,27 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/jmoiron/sqlx"
)
func Health(db *sqlx.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
w.Header().Set("Content-Type", "application/json")
if err := db.PingContext(ctx); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "db": "error"})
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "db": "ok"})
}
}

View File

@@ -0,0 +1,32 @@
package middleware
import (
"encoding/json"
"net/http"
"github.com/jmoiron/sqlx"
"emly-api-go/internal/config"
)
func APIKeyAuth(_ *sqlx.DB) func(http.Handler) http.Handler {
cfg := config.Load()
allowed := make(map[string]struct{}, len(cfg.APIKeys))
for _, k := range cfg.APIKeys {
allowed[k] = struct{}{}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if _, ok := allowed[key]; !ok {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"})
return
}
next.ServeHTTP(w, r)
})
}
}

View File

@@ -0,0 +1,26 @@
package models
import (
"encoding/json"
"time"
)
type BugReportStatus string
const (
BugReportStatusOpen BugReportStatus = "open"
BugReportStatusInProgress BugReportStatus = "in_progress"
BugReportStatusResolved BugReportStatus = "resolved"
BugReportStatusClosed BugReportStatus = "closed"
)
type BugReport struct {
ID int64 `db:"id" json:"id"`
UserID *int64 `db:"user_id" json:"user_id"`
Title string `db:"title" json:"title"`
Description string `db:"description" json:"description"`
Status BugReportStatus `db:"status" json:"status"`
SystemInfo json.RawMessage `db:"system_info" json:"system_info,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

View File

@@ -0,0 +1,21 @@
package models
import "time"
type FileRole string
const (
FileRoleAttachment FileRole = "attachment"
FileRoleScreenshot FileRole = "screenshot"
FileRoleLog FileRole = "log"
)
type BugReportFile struct {
ID int64 `db:"id" json:"id"`
BugReportID int64 `db:"bug_report_id" json:"bug_report_id"`
Filename string `db:"filename" json:"filename"`
MimeType string `db:"mime_type" json:"mime_type"`
Role FileRole `db:"role" json:"role"`
Data []byte `db:"data" json:"-"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}

View File

@@ -0,0 +1,9 @@
package models
import "time"
type RateLimitHWID struct {
HWID string `db:"hwid" json:"hwid"`
Requests int `db:"requests" json:"requests"`
WindowStart time.Time `db:"window_start" json:"window_start"`
}

View File

@@ -0,0 +1,11 @@
package models
import "time"
type Session struct {
ID string `db:"id" json:"id"`
UserID int64 `db:"user_id" json:"user_id"`
Token string `db:"token" json:"token"`
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}

21
internal/models/user.go Normal file
View File

@@ -0,0 +1,21 @@
package models
import "time"
type UserRole string
const (
UserRoleAdmin UserRole = "admin"
UserRoleUser UserRole = "user"
)
type User struct {
ID int64 `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Email string `db:"email" json:"email"`
PasswordHash string `db:"password_hash" json:"-"`
Role UserRole `db:"role" json:"role"`
Enabled bool `db:"enabled" json:"enabled"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}