package db import ( "context" "database/sql" "errors" "log/slog" "spend-sparrow/internal/types" "strings" "time" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) type Auth interface { InsertUser(ctx context.Context, user *types.User) error UpdateUser(ctx context.Context, user *types.User) error GetUserByEmail(ctx context.Context, email string) (*types.User, error) GetUser(ctx context.Context, userId uuid.UUID) (*types.User, error) DeleteUser(ctx context.Context, userId uuid.UUID) error InsertToken(ctx context.Context, token *types.Token) error GetToken(ctx context.Context, token string) (*types.Token, error) GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType types.TokenType) ([]*types.Token, error) DeleteToken(ctx context.Context, token string) error InsertSession(ctx context.Context, session *types.Session) error GetSession(ctx context.Context, sessionId string) (*types.Session, error) GetSessions(ctx context.Context, userId uuid.UUID) ([]*types.Session, error) DeleteSession(ctx context.Context, sessionId string) error DeleteOldSessions(ctx context.Context) error DeleteOldTokens(ctx context.Context) error } type AuthSqlite struct { db *sqlx.DB } func NewAuthSqlite(db *sqlx.DB) *AuthSqlite { return &AuthSqlite{db: db} } func (db AuthSqlite) InsertUser(ctx context.Context, user *types.User) error { _, err := db.db.ExecContext(ctx, ` INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, user.Id, user.Email, user.EmailVerified, user.EmailVerifiedAt, user.IsAdmin, user.Password, user.Salt, user.CreateAt) if err != nil { if strings.Contains(err.Error(), "email") { return ErrAlreadyExists } slog.Error("SQL error InsertUser", "err", err) return types.ErrInternal } return nil } func (db AuthSqlite) UpdateUser(ctx context.Context, user *types.User) error { _, err := db.db.ExecContext(ctx, ` UPDATE user SET email_verified = ?, email_verified_at = ?, password = ? WHERE user_id = ?`, user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id) if err != nil { slog.Error("SQL error UpdateUser", "err", err) return types.ErrInternal } return nil } func (db AuthSqlite) GetUserByEmail(ctx context.Context, email string) (*types.User, error) { var ( userId uuid.UUID emailVerified bool emailVerifiedAt *time.Time isAdmin bool password []byte salt []byte createdAt time.Time ) err := db.db.QueryRowContext(ctx, ` SELECT user_id, email_verified, email_verified_at, password, salt, created_at FROM user WHERE email = ?`, email).Scan(&userId, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } else { slog.Error("SQL error GetUser", "err", err) return nil, types.ErrInternal } } return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil } func (db AuthSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*types.User, error) { var ( email string emailVerified bool emailVerifiedAt *time.Time isAdmin bool password []byte salt []byte createdAt time.Time ) err := db.db.QueryRowContext(ctx, ` SELECT email, email_verified, email_verified_at, password, salt, created_at FROM user WHERE user_id = ?`, userId).Scan(&email, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } else { slog.Error("SQL error GetUser", "err", err) return nil, types.ErrInternal } } return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil } func (db AuthSqlite) DeleteUser(ctx context.Context, userId uuid.UUID) error { tx, err := db.db.BeginTx(ctx, nil) if err != nil { slog.Error("Could not start transaction", "err", err) return types.ErrInternal } _, err = tx.ExecContext(ctx, "DELETE FROM account WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() slog.Error("Could not delete accounts", "err", err) return types.ErrInternal } _, err = tx.ExecContext(ctx, "DELETE FROM token WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() slog.Error("Could not delete user tokens", "err", err) return types.ErrInternal } _, err = tx.ExecContext(ctx, "DELETE FROM session WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() slog.Error("Could not delete sessions", "err", err) return types.ErrInternal } _, err = tx.ExecContext(ctx, "DELETE FROM user WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() slog.Error("Could not delete user", "err", err) return types.ErrInternal } _, err = tx.ExecContext(ctx, "DELETE FROM treasure_chest WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() slog.Error("Could not delete user", "err", err) return types.ErrInternal } _, err = tx.ExecContext(ctx, "DELETE FROM \"transaction\" WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() slog.Error("Could not delete user", "err", err) return types.ErrInternal } err = tx.Commit() if err != nil { slog.Error("Could not commit transaction", "err", err) return types.ErrInternal } return nil } func (db AuthSqlite) InsertToken(ctx context.Context, token *types.Token) error { _, err := db.db.ExecContext(ctx, ` INSERT INTO token (user_id, session_id, type, token, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt) if err != nil { slog.Error("Could not insert token", "err", err) return types.ErrInternal } return nil } func (db AuthSqlite) GetToken(ctx context.Context, token string) (*types.Token, error) { var ( userId uuid.UUID sessionId string tokenType types.TokenType createdAtStr string expiresAtStr string createdAt time.Time expiresAt time.Time ) err := db.db.QueryRowContext(ctx, ` SELECT user_id, session_id, type, created_at, expires_at FROM token WHERE token = ?`, token).Scan(&userId, &sessionId, &tokenType, &createdAtStr, &expiresAtStr) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Info("Token not found", "token", token) return nil, ErrNotFound } else { slog.Error("Could not get token", "err", err) return nil, types.ErrInternal } } createdAt, err = time.Parse(time.RFC3339, createdAtStr) if err != nil { slog.Error("Could not parse token.created_at", "err", err) return nil, types.ErrInternal } expiresAt, err = time.Parse(time.RFC3339, expiresAtStr) if err != nil { slog.Error("Could not parse token.expires_at", "err", err) return nil, types.ErrInternal } return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil } func (db AuthSqlite) GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) { query, err := db.db.QueryContext(ctx, ` SELECT token, created_at, expires_at FROM token WHERE user_id = ? AND type = ?`, userId, tokenType) if err != nil { slog.Error("Could not get token", "err", err) return nil, types.ErrInternal } return getTokensFromQuery(query, userId, "", tokenType) } func (db AuthSqlite) GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType types.TokenType) ([]*types.Token, error) { query, err := db.db.QueryContext(ctx, ` SELECT token, created_at, expires_at FROM token WHERE session_id = ? AND type = ?`, sessionId, tokenType) if err != nil { slog.Error("Could not get token", "err", err) return nil, types.ErrInternal } return getTokensFromQuery(query, uuid.Nil, sessionId, tokenType) } func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) { var tokens []*types.Token hasRows := false for query.Next() { hasRows = true var ( token string createdAtStr string expiresAtStr string createdAt time.Time expiresAt time.Time ) err := query.Scan(&token, &createdAtStr, &expiresAtStr) if err != nil { slog.Error("Could not scan token", "err", err) return nil, types.ErrInternal } createdAt, err = time.Parse(time.RFC3339, createdAtStr) if err != nil { slog.Error("Could not parse token.created_at", "err", err) return nil, types.ErrInternal } expiresAt, err = time.Parse(time.RFC3339, expiresAtStr) if err != nil { slog.Error("Could not parse token.expires_at", "err", err) return nil, types.ErrInternal } tokens = append(tokens, types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt)) } if !hasRows { return nil, ErrNotFound } return tokens, nil } func (db AuthSqlite) DeleteToken(ctx context.Context, token string) error { _, err := db.db.ExecContext(ctx, "DELETE FROM token WHERE token = ?", token) if err != nil { slog.Error("Could not delete token", "err", err) return types.ErrInternal } return nil } func (db AuthSqlite) InsertSession(ctx context.Context, session *types.Session) error { _, err := db.db.ExecContext(ctx, ` INSERT INTO session (session_id, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt) if err != nil { slog.Error("Could not insert new session", "err", err) return types.ErrInternal } return nil } func (db AuthSqlite) GetSession(ctx context.Context, sessionId string) (*types.Session, error) { var ( userId uuid.UUID createdAt time.Time expiresAt time.Time ) err := db.db.QueryRowContext(ctx, ` SELECT user_id, created_at, expires_at FROM session WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt) if err != nil { slog.Warn("Session not found", "session-id", sessionId, "err", err) return nil, ErrNotFound } return types.NewSession(sessionId, userId, createdAt, expiresAt), nil } func (db AuthSqlite) GetSessions(ctx context.Context, userId uuid.UUID) ([]*types.Session, error) { var sessions []*types.Session err := db.db.SelectContext(ctx, &sessions, ` SELECT * FROM session WHERE user_id = ?`, userId) if err != nil { slog.Error("Could not get sessions", "err", err) return nil, types.ErrInternal } return sessions, nil } func (db AuthSqlite) DeleteSession(ctx context.Context, sessionId string) error { if sessionId != "" { _, err := db.db.ExecContext(ctx, "DELETE FROM session WHERE session_id = ?", sessionId) if err != nil { slog.Error("Could not delete session", "err", err) return types.ErrInternal } } return nil } func (db AuthSqlite) DeleteOldSessions(ctx context.Context) error { _, err := db.db.ExecContext(ctx, ` DELETE FROM session WHERE expires_at < datetime('now')`) if err != nil { slog.Error("Could not delete old sessions", "err", err) return types.ErrInternal } return nil } func (db AuthSqlite) DeleteOldTokens(ctx context.Context) error { _, err := db.db.ExecContext(ctx, ` DELETE FROM token WHERE expires_at < datetime('now')`) if err != nil { slog.Error("Could not delete old tokens", "err", err) return types.ErrInternal } return nil }