// Package auth provides authentication-related API endpoints for the Triggersmith application. // It handles login, logout, and user management operations. package api_auth import ( "fmt" "encoding/json" "net/http" "time" "git.oblat.lv/alex/triggerssmith/internal/auth" "git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/server" "github.com/go-chi/chi/v5" "github.com/golang-jwt/jwt/v5" ) func setRefreshCookie(w http.ResponseWriter, token string, ttl time.Duration, secure bool) { http.SetCookie(w, &http.Cookie{ Name: "refresh_token", Value: token, Path: "/api/auth/refresh", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: int(ttl.Seconds()), Secure: secure, }) } type authHandler struct { cfg *config.Config a *auth.Service } func MustRoute(config *config.Config, authService *auth.Service) func(chi.Router) { if config == nil { panic("config is nil") } if authService == nil { panic("authService is nil") } h := &authHandler{ cfg: config, a: authService, } return func(r chi.Router) { r.Get("/getUserData", h.handleGetUserData) // legacy support r.Post("/register", h.handleRegister) r.Post("/login", h.handleLogin) r.Post("/logout", h.handleLogout) // !requires authentication r.Post("/refresh", h.handleRefresh) // !requires authentication r.Get("/me", h.handleMe) // !requires authentication r.Get("/get-user-data", h.handleGetUserData) r.Post("/revoke", h.handleRevoke) // not implemented } } type registerRequest struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` } type registerResponse struct { UserID int64 `json:"id"` Username string `json:"username"` } func (h *authHandler) handleRegister(w http.ResponseWriter, r *http.Request) { var req registerRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, "Invalid request payload", http.StatusBadRequest) return } user, err := h.a.Register(req.Username, req.Email, req.Password) if err != nil { http.Error(w, "Registration failed", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(registerResponse{ UserID: user.ID, Username: user.Username, }) if err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) return } } type loginRequest struct { Username string `json:"username"` Password string `json:"password"` } type loginResponse struct { Token string `json:"accessToken"` } func (h *authHandler) handleLogin(w http.ResponseWriter, r *http.Request) { var req loginRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, "Invalid request payload", http.StatusBadRequest) return } tokens, err := h.a.Login(req.Username, req.Password) if err != nil { http.Error(w, "Authentication failed", http.StatusUnauthorized) return } setRefreshCookie(w, tokens.Refresh, h.cfg.Auth.RefreshTokenTTL, false) w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(loginResponse{Token: tokens.Access}) if err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) return } } func (h *authHandler) handleLogout(w http.ResponseWriter, r *http.Request) { claims, err := h.a.AuthenticateRequest(r) if err != nil { w.WriteHeader(http.StatusUnauthorized) return } rjti := claims.(jwt.MapClaims)["rjti"].(string) err = h.a.Logout(rjti) if err != nil { http.Error(w, "Failed to logout, taking cookie anyways", http.StatusInternalServerError) } http.SetCookie(w, &http.Cookie{ Name: "refresh_token", Value: "", MaxAge: -1, Path: "/api/users/refresh", HttpOnly: true, SameSite: http.SameSiteLaxMode, }) if err == nil { w.WriteHeader(http.StatusOK) } } type meResponse struct { UserID int64 `json:"id"` Username string `json:"username"` Email string `json:"email"` } func (h *authHandler) handleMe(w http.ResponseWriter, r *http.Request) { refresh_token_cookie, err := r.Cookie("refresh_token") if err != nil { w.WriteHeader(http.StatusUnauthorized) return } userID, err := h.a.ValidateRefreshToken(refresh_token_cookie.Value) if err != nil { w.WriteHeader(http.StatusUnauthorized) return } user, err := h.a.Get("id", fmt.Sprint(userID)) if err != nil { http.Error(w, "Failed to get user", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(meResponse{ UserID: user.ID, Username: user.Username, Email: user.Email, }) if err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) return } } type GetUserDataResponse meResponse func (h *authHandler) handleGetUserData(w http.ResponseWriter, r *http.Request) { by := r.URL.Query().Get("by") value := r.URL.Query().Get("value") if value == "" { value = r.URL.Query().Get(by) } user, err := h.a.Get(by, value) if err != nil { http.Error(w, "Failed to get user", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(meResponse{ UserID: user.ID, Username: user.Username, Email: user.Email, }) if err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) return } } func (h *authHandler) handleRevoke(w http.ResponseWriter, r *http.Request) { server.NotImplemented(w) } func (h *authHandler) handleRefresh(w http.ResponseWriter, r *http.Request) { server.NotImplemented(w) }