Files
triggerssmith/api/auth/handle.go

221 lines
5.6 KiB
Go

// Package auth provides authentication-related API endpoints for the Triggersmith application.
// It handles login, logout, and user management operations.
package api_auth
import (
"encoding/json"
"fmt"
"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 uint `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
}
w.WriteHeader(http.StatusCreated)
}
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 uint `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)
}