Files
move-data-nas/internal/api/handlers_auth.go
T
darroyo 8e08c73f60 feat: complete SyncServer implementation
Full-stack Go monolith with embedded React frontend for orchestrating
rsync-over-SSH file synchronization with Wake-on-LAN support.

Features:
- JWT auth (HS256) with bcrypt password hashing
- CRUD for machines (with WoL config) and sync_pairs
- Ed25519 SSH key generation and known_hosts management
- WoL magic packet sender + TCP-connect waiter with backoff
- Sync engine: rsync subprocess, per-pair job queue, progress parsing
- Homebrew cron parser for scheduled syncs
- SSE stream for live job status (queued/waking_up/running/success/failed)
- React+TS+Vite+Tailwind SPA embedded via embed.FS
- Debian packaging with systemd unit, postinst/prerm/postrm

Tech stack:
- Go 1.22+ (CGO_ENABLED=0, pure SQLite via modernc.org/sqlite)
- chi router for HTTP API
- TypeScript + React 18 + Tailwind CSS frontend
- Cross-compiled to Linux amd64 for Proxmox LXC deployment

Tests: wol (MAC parsing, magic packet), syncengine/queue, scheduler/cron
2026-07-07 15:03:22 -04:00

108 lines
2.5 KiB
Go

package api
import (
"database/sql"
"encoding/json"
"net/http"
"github.com/syncserver/internal/auth"
)
type AuthHandler struct {
db *sql.DB
}
func NewAuthHandler(db *sql.DB) *AuthHandler {
return &AuthHandler{db: db}
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
User UserResponse `json:"user"`
ExpiresAt string `json:"expires_at"`
}
type UserResponse struct {
ID int64 `json:"id"`
Username string `json:"username"`
Role string `json:"role"`
}
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
row := h.db.QueryRow(
"SELECT id, username, password_hash, role FROM users WHERE username = ?",
req.Username,
)
var u struct {
ID int64
Username string
PasswordHash string
Role string
}
if err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role); err != nil {
http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
return
}
if !auth.VerifyPassword([]byte(u.PasswordHash), req.Password) {
http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
return
}
jwtMgr := auth.GetJWTManager()
if jwtMgr == nil {
http.Error(w, `{"error":"server misconfigured"}`, http.StatusInternalServerError)
return
}
token, expiresAt, err := jwtMgr.Generate(u.ID, u.Username, u.Role)
if err != nil {
http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError)
return
}
auth.SetAuthCookie(w, token, expiresAt)
resp := LoginResponse{
User: UserResponse{
ID: u.ID,
Username: u.Username,
Role: u.Role,
},
ExpiresAt: expiresAt.Format("2006-01-02T15:04:05Z07:00"),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
auth.ClearAuthCookie(w)
w.WriteHeader(http.StatusNoContent)
}
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
claims := auth.GetClaims(r.Context())
if claims == nil {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
resp := UserResponse{
ID: claims.UserID,
Username: claims.Username,
Role: claims.Role,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"user": resp})
}