202 lines
6.4 KiB
Go
202 lines
6.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"terminalShop/api/middleware"
|
|
"terminalShop/pkg/auth"
|
|
"terminalShop/pkg/database"
|
|
"terminalShop/pkg/models"
|
|
"terminalShop/pkg/utils"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func authLog() *slog.Logger { return slog.With("component", "auth") }
|
|
|
|
// AuthHandler handles authentication-related requests
|
|
type AuthHandler struct {
|
|
jwtManager *auth.JWTManager
|
|
authFingerprintKey string
|
|
}
|
|
|
|
// NewAuthHandler creates a new auth handler
|
|
func NewAuthHandler(jwtManager *auth.JWTManager, authFingerprintKey string) *AuthHandler {
|
|
return &AuthHandler{
|
|
jwtManager: jwtManager,
|
|
authFingerprintKey: authFingerprintKey,
|
|
}
|
|
}
|
|
|
|
// Type to represent a request to exchange SSH fingerprint for JWT
|
|
type TokenRequest struct {
|
|
Fingerprint string `json:"fingerprint"`
|
|
SSHPublicKey string `json:"ssh_public_key"`
|
|
ClientSecret string `json:"client_secret"`
|
|
}
|
|
|
|
// Type to represent the JWT token response
|
|
type TokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
TokenType string `json:"token_type"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
User models.PublicUser `json:"user"`
|
|
}
|
|
|
|
func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) {
|
|
db := database.GetDB().WithContext(r.Context())
|
|
|
|
var req TokenRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
middleware.RecordAuthAttempt("invalid_json")
|
|
utils.RespondError(w, http.StatusBadRequest, "INVALID_JSON", "invalid request body", nil)
|
|
return
|
|
}
|
|
|
|
// validate shared secret
|
|
if req.ClientSecret != h.authFingerprintKey {
|
|
middleware.RecordAuthAttempt("invalid_secret")
|
|
authLog().Warn("invalid client secret", "remote", r.RemoteAddr)
|
|
utils.RespondError(w, http.StatusUnauthorized, "INVALID_SECRET", "invalid client secret", nil)
|
|
return
|
|
}
|
|
|
|
// Return if no fingerprint is found
|
|
if req.Fingerprint == "" {
|
|
middleware.RecordAuthAttempt("missing_fingerprint")
|
|
utils.RespondError(w, http.StatusBadRequest, "MISSING_FINGERPRINT", "fingerprint is required", nil)
|
|
return
|
|
}
|
|
|
|
// look up user by his fingies
|
|
var user models.User
|
|
err := db.Where("ssh_key_fingerprint = ?", req.Fingerprint).First(&user).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
if req.SSHPublicKey == "" {
|
|
middleware.RecordAuthAttempt("user_not_found")
|
|
authLog().Warn("user not found and no public key for registration", "fingerprint", req.Fingerprint)
|
|
utils.RespondError(w, http.StatusUnauthorized, "USER_NOT_FOUND", "no user found for this fingerprint", nil)
|
|
return
|
|
}
|
|
user = models.User{
|
|
SSHKeyFingerprint: req.Fingerprint,
|
|
SSHPublicKey: req.SSHPublicKey,
|
|
}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
middleware.RecordAuthAttempt("error")
|
|
authLog().Error("failed to create user", "error", err)
|
|
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to create user", nil)
|
|
return
|
|
}
|
|
middleware.RecordAuthAttempt("registered")
|
|
authLog().Info("user registered", "user_id", user.ID)
|
|
} else if err != nil {
|
|
middleware.RecordAuthAttempt("error")
|
|
authLog().Error("failed to look up user", "error", err)
|
|
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to look up user", nil)
|
|
return
|
|
}
|
|
|
|
token, err := h.jwtManager.GenerateToken(user.ID, user.Email, user.Name)
|
|
if err != nil {
|
|
middleware.RecordAuthAttempt("error")
|
|
authLog().Error("failed to generate token", "user_id", user.ID, "error", err)
|
|
utils.RespondError(w, http.StatusInternalServerError, "TOKEN_ERROR", "failed to generate token", nil)
|
|
return
|
|
}
|
|
|
|
middleware.RecordAuthAttempt("success")
|
|
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
|
|
"access_token": token,
|
|
"token_type": "Bearer",
|
|
"expires_in": 1800, // 30 minutes in seconds
|
|
"user": user.ToPublic(),
|
|
})
|
|
}
|
|
|
|
// RegisterRequest represents a user registration request
|
|
type RegisterRequest struct {
|
|
SSHPublicKey string `json:"ssh_public_key"`
|
|
SSHKeyFingerprint string `json:"ssh_key_fingerprint"`
|
|
Name string `json:"name,omitempty"` // Optional
|
|
Email string `json:"email,omitempty"` // Optional
|
|
}
|
|
|
|
// RegisterWithSSHKey registers a new user with their SSH public key
|
|
// No username required - following terminal.shop pattern
|
|
func (h *AuthHandler) RegisterWithSSHKey(w http.ResponseWriter, r *http.Request) {
|
|
db := database.GetDB().WithContext(r.Context())
|
|
|
|
var req RegisterRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
utils.RespondError(w, http.StatusBadRequest, "INVALID_JSON", "invalid request body", nil)
|
|
return
|
|
}
|
|
|
|
// Validate required fields
|
|
errors := map[string]string{}
|
|
if req.SSHPublicKey == "" {
|
|
errors["ssh_public_key"] = "SSH public key is required"
|
|
}
|
|
|
|
if req.SSHKeyFingerprint == "" {
|
|
errors["ssh_key_fingerprint"] = "SSH key fingerprint is required"
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
utils.RespondError(w, http.StatusBadRequest, "VALIDATION_ERROR", "validation failed", map[string]interface{}{
|
|
"errors": errors,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if SSH key is already registered
|
|
var existing models.User
|
|
err := db.Where("ssh_key_fingerprint = ?", req.SSHKeyFingerprint).First(&existing).Error
|
|
if err == nil {
|
|
utils.RespondError(w, http.StatusConflict, "SSH_KEY_REGISTERED", "SSH key already registered", nil)
|
|
return
|
|
}
|
|
|
|
// Create new user (no username required!)
|
|
user := models.User{
|
|
SSHPublicKey: req.SSHPublicKey,
|
|
SSHKeyFingerprint: req.SSHKeyFingerprint,
|
|
Name: req.Name,
|
|
Email: req.Email,
|
|
}
|
|
|
|
if err := db.Create(&user).Error; err != nil {
|
|
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to create user", nil)
|
|
return
|
|
}
|
|
|
|
utils.RespondSuccess(w, http.StatusCreated, map[string]interface{}{
|
|
"user": user.ToPublic(),
|
|
})
|
|
}
|
|
|
|
// GetUserBySSHKey retrieves a user by their SSH key fingerprint
|
|
func (h *AuthHandler) GetUserBySSHKey(w http.ResponseWriter, r *http.Request) {
|
|
db := database.GetDB().WithContext(r.Context())
|
|
|
|
fingerprint := r.URL.Query().Get("fingerprint")
|
|
if fingerprint == "" {
|
|
utils.RespondError(w, http.StatusBadRequest, "MISSING_FINGERPRINT", "fingerprint query parameter is required", nil)
|
|
return
|
|
}
|
|
|
|
var user models.User
|
|
err := db.Where("ssh_key_fingerprint = ?", fingerprint).First(&user).Error
|
|
if err != nil {
|
|
utils.RespondError(w, http.StatusNotFound, "USER_NOT_FOUND", "user not found", nil)
|
|
return
|
|
}
|
|
|
|
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
|
|
"user": user.ToPublic(),
|
|
})
|
|
}
|