package handlers import ( "bytes" "context" "encoding/json" "errors" "fmt" "log/slog" "net/http" "time" "git.optclblast.xyz/draincloud/draincloud-core/internal/common" "git.optclblast.xyz/draincloud/draincloud-core/internal/domain" "git.optclblast.xyz/draincloud/draincloud-core/internal/errs" "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) type LogonHandler struct { *handler.BaseHandler authStorage storage.AuthStorage } func NewLogonHandler( authStorage storage.AuthStorage, ) *LogonHandler { h := &LogonHandler{ authStorage: authStorage, BaseHandler: handler.New(). WithName("logonv1"). WithRequiredResolveParams(), } h.WithProcessFunc(h.process) return h } func (h *LogonHandler) process(ctx context.Context, req *common.Request, w handler.Writer) error { logger.Debug(ctx, "[Logon] new request") body := new(domain.LogonRequest) err := json.Unmarshal(req.Body, body) if err != nil { 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 } session, err := h.getSession(ctx, req) if err != nil && !errors.Is(err, http.ErrNoCookie) { return err } if session != nil { if err := validateSession(ctx, req, session); err != nil { // TODO add audit log entry return err } logger.Debug(ctx, "[login] user is already logged in", slog.String("session_id", session.ID.String())) w.Write(ctx, &domain.LogonResponse{ Ok: true, }) return nil } 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 } func (h *LogonHandler) login(ctx context.Context, req *domain.LogonRequest, session *models.Session, w handler.Writer) (*domain.LogonResponse, error) { 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) } user, err := h.authStorage.GetUserByLogin(ctx, req.Login) 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") return nil, errs.ErrorAccessDenied } 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) } w.SetCookie(sessionTokenCookie, sessionToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, true) csrfToken, err := generateSessionToken(100) if err != nil { return nil, fmt.Errorf("failed to generate a csrf token: %w", err) } w.SetCookie(csrfTokenCookie, csrfToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, false) sessionID, err := uuid.NewV7() if err != nil { return nil, fmt.Errorf("failed to generate session id: %w", err) } if _, err = h.authStorage.AddSession(ctx, &models.Session{ ID: sessionID, 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 return &domain.LogonResponse{ Ok: true, }, nil } func (h *LogonHandler) getSession(ctx context.Context, req *common.Request) (*models.Session, error) { token, err := common.GetValue[string](req.Metadata, sessionTokenCookie) if err != nil { return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err) } csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie) 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") } session, err := h.authStorage.GetSession(ctx, token) if err != nil { return nil, fmt.Errorf("failed to fetch session from repo: %w", err) } return session, nil } func validateSession(ctx context.Context, req *common.Request, session *models.Session) error { if session == nil { return errs.ErrorAccessDenied } csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie) if err != nil { return fmt.Errorf("failed to fetch csrf cookie from request: %w", err) } if session.CsrfToken != csrfToken { return errs.ErrorAccessDenied } if session.ExpiredAt.Before(time.Now()) { return errs.ErrorSessionExpired } return nil }