diff --git a/api/auth/handle.go b/api/auth/handle.go index 55f9209..54de7ce 100644 --- a/api/auth/handle.go +++ b/api/auth/handle.go @@ -3,35 +3,217 @@ 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) func(chi.Router) { +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("/login", h.handleLogin) - r.Get("/logout", h.handleLogout) - r.Get("/me", h.handleMe) - r.Get("/revoke", h.handleRevoke) + 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 } } -func (h *authHandler) handleLogin(w http.ResponseWriter, r *http.Request) {} +type registerRequest struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} -func (h *authHandler) handleLogout(w http.ResponseWriter, r *http.Request) {} +type registerResponse struct { + UserID int64 `json:"id"` + Username string `json:"username"` +} -func (h *authHandler) handleMe(w http.ResponseWriter, r *http.Request) {} +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 + } -func (h *authHandler) handleRevoke(w http.ResponseWriter, r *http.Request) {} + 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) +}