223 lines
5.6 KiB
Go
223 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"
|
|
"log/slog"
|
|
"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 {
|
|
slog.Error("Failed to register user", "error", err)
|
|
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)
|
|
}
|