add initial project structure with configuration, models, and API key authentication
This commit is contained in:
34
internal/config/config.go
Normal file
34
internal/config/config.go
Normal 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,
|
||||
}
|
||||
}
|
||||
23
internal/database/database.go
Normal file
23
internal/database/database.go
Normal 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
|
||||
}
|
||||
24
internal/handlers/example.go
Normal file
24
internal/handlers/example.go
Normal 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),
|
||||
})
|
||||
}
|
||||
27
internal/handlers/health.go
Normal file
27
internal/handlers/health.go
Normal 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"})
|
||||
}
|
||||
}
|
||||
32
internal/middleware/apikey.go
Normal file
32
internal/middleware/apikey.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
26
internal/models/bug_report.go
Normal file
26
internal/models/bug_report.go
Normal 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"`
|
||||
}
|
||||
21
internal/models/bug_report_file.go
Normal file
21
internal/models/bug_report_file.go
Normal 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"`
|
||||
}
|
||||
9
internal/models/rate_limit_hwid.go
Normal file
9
internal/models/rate_limit_hwid.go
Normal 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"`
|
||||
}
|
||||
11
internal/models/session.go
Normal file
11
internal/models/session.go
Normal 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
21
internal/models/user.go
Normal 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user