mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-26 10:18:46 +01:00
Auth handler used FirstOrCreate which couldn't see soft-deleted keys (GORM filters by deleted_at IS NULL) but also couldn't insert (unique index still covers deleted rows), returning 500 on every login after a key deletion. Use Unscoped lookup and restore soft-deleted keys. Splash screen also got permanently stuck when auth failed because splashViewInitCmd was never fired, leaving splash.dataReady false.
211 lines
6.4 KiB
Go
211 lines
6.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"terminalShop/pkg/auth"
|
|
"terminalShop/pkg/database"
|
|
"terminalShop/pkg/models"
|
|
"terminalShop/pkg/utils"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// 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()
|
|
|
|
var req TokenRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
utils.RespondError(w, http.StatusBadRequest, "INVALID_JSON", "invalid request body", nil)
|
|
return
|
|
}
|
|
|
|
// validate shared secret
|
|
if req.ClientSecret != h.authFingerprintKey {
|
|
utils.RespondError(w, http.StatusUnauthorized, "INVALID_SECRET", "invalid client secret", nil)
|
|
return
|
|
}
|
|
|
|
// Return if no fingerprint is found
|
|
if req.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 == "" {
|
|
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 {
|
|
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to create user", nil)
|
|
return
|
|
}
|
|
} else if err != nil {
|
|
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to look up user", nil)
|
|
return
|
|
}
|
|
|
|
if req.SSHPublicKey != "" {
|
|
var sshKey models.SSHKey
|
|
// Use Unscoped to include soft-deleted rows, avoiding the
|
|
// UNIQUE constraint conflict when a key is re-registered.
|
|
err := db.Unscoped().Where("fingerprint = ?", req.Fingerprint).First(&sshKey).Error
|
|
if err != nil {
|
|
// Key doesn't exist at all — create it
|
|
sshKey = models.SSHKey{
|
|
UserID: user.ID,
|
|
Fingerprint: req.Fingerprint,
|
|
PublicKey: req.SSHPublicKey,
|
|
}
|
|
if err := db.Create(&sshKey).Error; err != nil {
|
|
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to save ssh key", nil)
|
|
return
|
|
}
|
|
} else if sshKey.DeletedAt.Valid {
|
|
// Key was soft-deleted — restore it
|
|
sshKey.DeletedAt = gorm.DeletedAt{}
|
|
sshKey.PublicKey = req.SSHPublicKey
|
|
sshKey.UserID = user.ID
|
|
if err := db.Unscoped().Save(&sshKey).Error; err != nil {
|
|
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to restore ssh key", nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
token, err := h.jwtManager.GenerateToken(user.ID, user.Email, user.Name)
|
|
if err != nil {
|
|
utils.RespondError(w, http.StatusInternalServerError, "TOKEN_ERROR", "failed to generate token", nil)
|
|
return
|
|
}
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
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(),
|
|
})
|
|
}
|