// Copyright (c) 2025 Nikolai Papin // // This file is part of Easywish // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See // the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package services import ( "easywish/internal/database" errs "easywish/internal/errors" "easywish/internal/models" "easywish/internal/utils" "easywish/internal/utils/enums" "github.com/jackc/pgerrcode" "github.com/jackc/pgx/v5" "go.uber.org/zap" ) type AuthService interface { RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) RegistrationComplete(request models.RegistrationCompleteRequest) (*models.RegistrationCompleteResponse, error) Login(request models.LoginRequest) (*models.LoginResponse, error) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) } type authServiceImpl struct { log *zap.Logger dbctx database.DbContext } func NewAuthService(_log *zap.Logger, _dbctx database.DbContext) AuthService { return &authServiceImpl{log: _log, dbctx: _dbctx} } func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) { var user database.User var generatedCode string helper, db, _ := database.NewDbHelperTransaction(a.dbctx) defer helper.Rollback() var err error if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil { if errs.IsPgErr(err, pgerrcode.UniqueViolation) { a.log.Warn( "Attempted registration for a taken username", zap.String("username", request.Username), zap.Error(err)) return false, errs.ErrUsernameTaken } a.log.Error("Failed to add user to database", zap.Error(err)) return false, errs.ErrServerError } if _, err := db.TXQueries.GetUserByEmail(db.CTX, *request.Email); err == nil { a.log.Warn( "Attempted registration for a taken email", zap.String("email", *request.Email)) return false, errs.ErrEmailTaken } else if !errs.IsPgErr(err, pgerrcode.NoData) { a.log.Error( "Failed to check if email is not taken", zap.String("email", *request.Email), zap.Error(err)) return false, errs.ErrServerError } else { a.log.Debug("Verified that email is not taken", zap.String("email", *request.Email)) } a.log.Info( "Registraion of a new user", zap.String("username", user.Username), zap.Int64("id", user.ID)) if _, err = db.TXQueries.CreateLoginInformation(db.CTX, database.CreateLoginInformationParams{ UserID: user.ID, Email: request.Email, Password: request.Password, // Hashed in database }); err != nil { a.log.Error("Failed to add login information for user to database", zap.Error(err)) return false, errs.ErrServerError } if generatedCode, err = utils.GenerateSecure6DigitNumber(); err != nil { a.log.Error("Failed to generate a registration code", zap.Error(err)) return false, errs.ErrServerError } if _, err = db.TXQueries.CreateConfirmationCode(db.CTX, database.CreateConfirmationCodeParams{ UserID: user.ID, CodeType: int32(enums.RegistrationCodeType), Code: generatedCode, // Hashed in database }); err != nil { a.log.Error("Failed to add registration code to database", zap.Error(err)) return false, errs.ErrServerError } a.log.Info("Registered a new user", zap.String("username", user.Username)) helper.Commit() a.log.Debug("Declated registration code for a new user", zap.String("username", user.Username), zap.String("code", generatedCode)) // TODO: Send verification email return true, nil } func (a *authServiceImpl) RegistrationComplete(request models.RegistrationCompleteRequest) (*models.RegistrationCompleteResponse, error) { var user database.User var profile database.Profile var session database.Session var confirmationCode database.ConfirmationCode var accessToken, refreshToken string var err error helper, db, _ := database.NewDbHelperTransaction(a.dbctx) user, err = db.TXQueries.GetUserByUsername(db.CTX, request.Username) if err != nil { if errs.IsPgErr(err, pgerrcode.NoData) { a.log.Warn( "Could not find user attempting to complete registration with given username", zap.String("username", request.Username), zap.Error(err)) return nil, errs.ErrUserNotFound } a.log.Error( "Failed to get user", zap.String("username", request.Username), zap.Error(err)) return nil, errs.ErrServerError } confirmationCode, err = db.TXQueries.GetConfirmationCodeByCode(db.CTX, database.GetConfirmationCodeByCodeParams{ UserID: user.ID, CodeType: int32(enums.RegistrationCodeType), Code: request.VerificationCode, }) if err != nil { if errs.IsPgErr(err, pgerrcode.NoData) { a.log.Warn( "User supplied unexistent confirmation code for completing registration", zap.String("username", user.Username), zap.String("code", request.VerificationCode), zap.Error(err)) return nil, errs.ErrForbidden } a.log.Error( "Failed to acquire specified registration code", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{ ID: confirmationCode.ID, Used: utils.NewPointer(true), }) if err != nil { a.log.Error( "Failed to update the user's registration code used state", zap.String("username", user.Username), zap.Int64("confirmation_code_id", confirmationCode.ID), zap.Error(err)) return nil, errs.ErrServerError } err = db.TXQueries.UpdateUser(db.CTX, database.UpdateUserParams{ Verified: utils.NewPointer(true), }) if err != nil { a.log.Error("Failed to update verified status for user", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } profile, err = db.TXQueries.CreateProfile(db.CTX, database.CreateProfileParams{ UserID: user.ID, Name: request.Name, AvatarUrl: request.AvatarUrl, }) if err != nil { a.log.Error("Failed to create profile for user", zap.String("username", user.Username), ) return nil, errs.ErrServerError } _, err = db.TXQueries.CreateProfileSettings(db.CTX, profile.ID) if err != nil { a.log.Error("Failed to create profile settings for user", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } // TODO: session info session, err = db.TXQueries.CreateSession(db.CTX, database.CreateSessionParams{ UserID: user.ID, Name: utils.NewPointer("First device"), Platform: utils.NewPointer("Unknown"), LatestIp: utils.NewPointer("Unknown"), }) if err != nil { a.log.Error( "Failed to create a new session during registration, rolling back registration", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } accessToken, refreshToken, err = utils.GenerateTokens(user.Username, session.Guid.String()) if err != nil { a.log.Error( "Failed to create tokens for newly registered user, rolling back registration", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } helper.Commit() a.log.Info( "User verified registration", zap.String("username", request.Username)) response := models.RegistrationCompleteResponse{Tokens: models.Tokens{ AccessToken: accessToken, RefreshToken: refreshToken, }} return &response, errs.ErrNotImplemented } // TODO: totp // TODO: banned user check func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginResponse, error) { var userRow database.GetUserByLoginCredentialsRow var session database.Session helper, db, _ := database.NewDbHelperTransaction(a.dbctx) defer helper.Rollback() var err error userRow, err = db.TXQueries.GetUserByLoginCredentials(db.CTX, database.GetUserByLoginCredentialsParams{ Username: request.Username, Password: request.Password, }) if err != nil { a.log.Warn( "Failed login attempt", zap.Error(err)) var returnedError error switch err { case pgx.ErrNoRows: returnedError = errs.ErrForbidden default: returnedError = errs.ErrServerError } return nil, returnedError } session, err = db.TXlessQueries.CreateSession(db.CTX, database.CreateSessionParams{ UserID: userRow.ID, Name: utils.NewPointer("New device"), Platform: utils.NewPointer("Unknown"), LatestIp: utils.NewPointer("Unknown"), }) if err != nil { a.log.Error( "Failed to create session for a new login", zap.String("username", userRow.Username), zap.Error(err)) return nil, errs.ErrServerError } accessToken, refreshToken, err := utils.GenerateTokens(userRow.Username, session.Guid.String()) if err != nil { a.log.Error( "Failed to generate tokens for a new login", zap.String("username", userRow.Username), zap.Error(err)) return nil, errs.ErrServerError } helper.Commit() response := models.LoginResponse{Tokens: models.Tokens{ AccessToken: accessToken, RefreshToken: refreshToken, }} return &response, nil } func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) { return nil, errs.ErrNotImplemented }