multica/server/internal/handler/auth.go

175 lines
4.1 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multica-ai/multica/server/internal/auth"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL *string `json:"avatar_url"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func userToResponse(u db.User) UserResponse {
return UserResponse{
ID: uuidToString(u.ID),
Name: u.Name,
Email: u.Email,
AvatarURL: textToPtr(u.AvatarUrl),
CreatedAt: timestampToString(u.CreatedAt),
UpdatedAt: timestampToString(u.UpdatedAt),
}
}
type LoginRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
type LoginResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
}
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
req.Name = strings.TrimSpace(req.Name)
if req.Email == "" {
writeError(w, http.StatusBadRequest, "email is required")
return
}
// Try to find existing user
user, err := h.Queries.GetUserByEmail(r.Context(), req.Email)
if err != nil {
if !isNotFound(err) {
writeError(w, http.StatusInternalServerError, "failed to load user")
return
}
// Create new user
name := req.Name
if name == "" {
name = req.Email
}
user, err = h.Queries.CreateUser(r.Context(), db.CreateUserParams{
Name: name,
Email: req.Email,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create user: "+err.Error())
return
}
} else if req.Name != "" && req.Name != user.Name {
user, err = h.Queries.UpdateUser(r.Context(), db.UpdateUserParams{
ID: user.ID,
Name: req.Name,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to update user")
return
}
}
// Generate JWT
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": uuidToString(user.ID),
"email": user.Email,
"name": user.Name,
"exp": time.Now().Add(72 * time.Hour).Unix(),
"iat": time.Now().Unix(),
})
tokenString, err := token.SignedString(auth.JWTSecret())
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to generate token")
return
}
writeJSON(w, http.StatusOK, LoginResponse{
Token: tokenString,
User: userToResponse(user),
})
}
func (h *Handler) GetMe(w http.ResponseWriter, r *http.Request) {
userID, ok := requireUserID(w, r)
if !ok {
return
}
user, err := h.Queries.GetUser(r.Context(), parseUUID(userID))
if err != nil {
writeError(w, http.StatusNotFound, "user not found")
return
}
writeJSON(w, http.StatusOK, userToResponse(user))
}
type UpdateMeRequest struct {
Name *string `json:"name"`
AvatarURL *string `json:"avatar_url"`
}
func (h *Handler) UpdateMe(w http.ResponseWriter, r *http.Request) {
userID, ok := requireUserID(w, r)
if !ok {
return
}
var req UpdateMeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
currentUser, err := h.Queries.GetUser(r.Context(), parseUUID(userID))
if err != nil {
writeError(w, http.StatusNotFound, "user not found")
return
}
name := currentUser.Name
if req.Name != nil {
name = strings.TrimSpace(*req.Name)
if name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
}
params := db.UpdateUserParams{
ID: currentUser.ID,
Name: name,
}
if req.AvatarURL != nil {
params.AvatarUrl = pgtype.Text{String: strings.TrimSpace(*req.AvatarURL), Valid: true}
}
updatedUser, err := h.Queries.UpdateUser(r.Context(), params)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to update user")
return
}
writeJSON(w, http.StatusOK, userToResponse(updatedUser))
}