2025-01-03 00:08:22 +00:00
|
|
|
package handlers
|
2024-11-23 08:52:06 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2025-01-03 00:08:22 +00:00
|
|
|
"context"
|
|
|
|
"encoding/json"
|
2024-11-23 08:52:06 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"log/slog"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
|
2024-11-23 08:52:06 +00:00
|
|
|
"git.optclblast.xyz/draincloud/draincloud-core/internal/domain"
|
2025-01-03 00:08:22 +00:00
|
|
|
"git.optclblast.xyz/draincloud/draincloud-core/internal/errs"
|
|
|
|
"git.optclblast.xyz/draincloud/draincloud-core/internal/handler"
|
2024-11-23 08:52:06 +00:00
|
|
|
"git.optclblast.xyz/draincloud/draincloud-core/internal/logger"
|
2025-01-03 00:08:22 +00:00
|
|
|
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
|
2024-11-23 08:52:06 +00:00
|
|
|
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models"
|
2024-12-16 05:08:37 +00:00
|
|
|
"github.com/google/uuid"
|
2024-11-23 08:52:06 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
)
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
type LogonHandler struct {
|
|
|
|
*handler.BaseHandler
|
|
|
|
authStorage storage.AuthStorage
|
|
|
|
}
|
2024-11-23 08:52:06 +00:00
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
func NewLogonHandler(
|
|
|
|
authStorage storage.AuthStorage,
|
|
|
|
) *LogonHandler {
|
|
|
|
h := &LogonHandler{
|
|
|
|
authStorage: authStorage,
|
|
|
|
BaseHandler: handler.New().
|
|
|
|
WithName("logonv1").
|
|
|
|
WithRequiredResolveParams(),
|
|
|
|
}
|
|
|
|
h.WithProcessFunc(h.process)
|
|
|
|
return h
|
|
|
|
}
|
2024-11-23 08:52:06 +00:00
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
func (h *LogonHandler) process(ctx context.Context, req *common.Request, w handler.Writer) error {
|
|
|
|
logger.Debug(ctx, "[Logon] new request")
|
2024-11-23 08:52:06 +00:00
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
body := new(domain.LogonRequest)
|
|
|
|
err := json.Unmarshal(req.Body, body)
|
2024-11-23 08:52:06 +00:00
|
|
|
if err != nil {
|
2025-01-03 00:08:22 +00:00
|
|
|
logger.Error(ctx, "[Logon] failed to bind request", logger.Err(err))
|
|
|
|
w.Write(ctx, map[string]string{
|
|
|
|
"error": "bad request",
|
|
|
|
}, handler.WithCode(http.StatusBadRequest))
|
|
|
|
return nil
|
2024-11-23 08:52:06 +00:00
|
|
|
}
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
session, err := h.getSession(ctx, req)
|
2024-11-23 08:52:06 +00:00
|
|
|
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
2025-01-03 00:08:22 +00:00
|
|
|
return err
|
2024-11-23 08:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if session != nil {
|
2025-01-03 00:08:22 +00:00
|
|
|
if err := validateSession(ctx, req, session); err != nil {
|
2024-11-23 08:52:06 +00:00
|
|
|
// TODO add audit log entry
|
2025-01-03 00:08:22 +00:00
|
|
|
return err
|
2024-11-23 08:52:06 +00:00
|
|
|
}
|
2024-12-16 05:08:37 +00:00
|
|
|
logger.Debug(ctx, "[login] user is already logged in", slog.String("session_id", session.ID.String()))
|
2025-01-03 00:08:22 +00:00
|
|
|
w.Write(ctx, &domain.LogonResponse{
|
2024-11-23 08:52:06 +00:00
|
|
|
Ok: true,
|
2025-01-03 00:08:22 +00:00
|
|
|
})
|
|
|
|
return nil
|
2024-11-23 08:52:06 +00:00
|
|
|
}
|
2025-01-03 00:08:22 +00:00
|
|
|
logger.Debug(ctx, "[login] session not founh. trying to authorize")
|
|
|
|
|
|
|
|
resp, err := h.login(ctx, body, session, w)
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(ctx, "[Logon] failed to login user", logger.Err(err))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Write(ctx, resp)
|
|
|
|
return nil
|
|
|
|
}
|
2024-11-23 08:52:06 +00:00
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
func (h *LogonHandler) login(ctx context.Context, req *domain.LogonRequest, session *models.Session, w handler.Writer) (*domain.LogonResponse, error) {
|
2024-11-23 08:52:06 +00:00
|
|
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(ctx, "[login] failed to generate password hash", logger.Err(err))
|
|
|
|
return nil, fmt.Errorf("failed to generate password hash: %w", err)
|
|
|
|
}
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
user, err := h.authStorage.GetUserByLogin(ctx, req.Login)
|
2024-11-23 08:52:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to fetch user by login: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if bytes.Equal(passwordHash, user.PasswordHash) {
|
|
|
|
logger.Warn(ctx, "[login] failed to login user. passwords hashes not equal")
|
2025-01-03 00:08:22 +00:00
|
|
|
return nil, errs.ErrorAccessDenied
|
2024-11-23 08:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sessionCreatedAt := time.Now()
|
|
|
|
sessionExpiredAt := sessionCreatedAt.Add(time.Hour * 24 * 7)
|
|
|
|
|
|
|
|
sessionToken, err := generateSessionToken(100)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to generate a session token: %w", err)
|
|
|
|
}
|
2025-01-03 00:08:22 +00:00
|
|
|
w.SetCookie(sessionTokenCookie, sessionToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, true)
|
2024-11-23 08:52:06 +00:00
|
|
|
|
|
|
|
csrfToken, err := generateSessionToken(100)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to generate a csrf token: %w", err)
|
|
|
|
}
|
2025-01-03 00:08:22 +00:00
|
|
|
w.SetCookie(csrfTokenCookie, csrfToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, false)
|
2024-11-23 08:52:06 +00:00
|
|
|
|
2024-12-16 05:08:37 +00:00
|
|
|
sessionID, err := uuid.NewV7()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to generate session id: %w", err)
|
|
|
|
}
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
if _, err = h.authStorage.AddSession(ctx, &models.Session{
|
2024-12-16 05:08:37 +00:00
|
|
|
ID: sessionID,
|
2024-11-23 08:52:06 +00:00
|
|
|
SessionToken: sessionToken,
|
|
|
|
CsrfToken: csrfToken,
|
|
|
|
UserID: user.ID,
|
|
|
|
CreatedAt: sessionCreatedAt,
|
|
|
|
ExpiredAt: sessionExpiredAt,
|
|
|
|
}); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to save session: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO add audit log entry
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
return &domain.LogonResponse{
|
2024-11-23 08:52:06 +00:00
|
|
|
Ok: true,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
func (h *LogonHandler) getSession(ctx context.Context, req *common.Request) (*models.Session, error) {
|
|
|
|
token, err := common.GetValue[string](req.Metadata, sessionTokenCookie)
|
2024-11-23 08:52:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err)
|
|
|
|
}
|
2025-01-03 00:08:22 +00:00
|
|
|
csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
|
2024-11-23 08:52:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(csrfToken) == 0 || len(token) == 0 {
|
|
|
|
return nil, fmt.Errorf("session token or csrf token is empty")
|
|
|
|
}
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
session, err := h.authStorage.GetSession(ctx, token)
|
2024-11-23 08:52:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to fetch session from repo: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return session, nil
|
|
|
|
}
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
func validateSession(ctx context.Context, req *common.Request, session *models.Session) error {
|
2024-11-23 08:52:06 +00:00
|
|
|
if session == nil {
|
2025-01-03 00:08:22 +00:00
|
|
|
return errs.ErrorAccessDenied
|
2024-11-23 08:52:06 +00:00
|
|
|
}
|
|
|
|
|
2025-01-03 00:08:22 +00:00
|
|
|
csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
|
2024-11-23 08:52:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if session.CsrfToken != csrfToken {
|
2025-01-03 00:08:22 +00:00
|
|
|
return errs.ErrorAccessDenied
|
2024-11-23 08:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if session.ExpiredAt.Before(time.Now()) {
|
2025-01-03 00:08:22 +00:00
|
|
|
return errs.ErrorSessionExpired
|
2024-11-23 08:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|