package auth import ( "context" "errors" "fmt" "log/slog" "sso/internal/domain/models" "sso/internal/lib/jwt" "sso/internal/lib/logger/sl" "sso/internal/storage" "time" "golang.org/x/crypto/bcrypt" ) type Auth struct { log *slog.Logger usrSaver UserSaver usrProvider UserProvider appProvider AppProvider tokenTTL time.Duration } type UserSaver interface { SaveUser(ctx context.Context, email string, passHash []byte) (uid int64, err error) } type UserProvider interface { User(ctx context.Context, email string) (models.User, error) IsAdmin(ctx context.Context, userID int64) (bool, error) } type AppProvider interface { App(ctx context.Context, appID int) (models.App, error) } var ( ErrInvalidCredentials = errors.New("invalid credentials") ErrInvalidAppID = errors.New("invalid app id") ErrUserExists = errors.New("user already exists") ErrUserNotFound = errors.New("user not found") ) // New returns a new instance of the Auth service. func New(log *slog.Logger, userSaver UserSaver, userProvider UserProvider, appProvider AppProvider, tokenTTL time.Duration) *Auth { return &Auth{ log: log, usrSaver: userSaver, usrProvider: userProvider, appProvider: appProvider, tokenTTL: tokenTTL, } } // Login checks if user with given credentials exists in the system and returns access token. // // If user exists, but password is incorrect, returns error. // If user doesn't exist, returns error. func (a *Auth) Login(ctx context.Context, email string, password string, appID int) (string, error) { const op = "services.auth.Login" log := a.log.With( slog.String("op", op), slog.String("username", email), ) log.Info("attempting to login user") user, err := a.usrProvider.User(ctx, email) if err != nil { if errors.Is(err, storage.ErrUserNotFound) { a.log.Warn("user not found", sl.Err(err)) return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials) } a.log.Error("failed to get user", sl.Err(err)) return "", fmt.Errorf("%s: %w", op, err) } if err := bcrypt.CompareHashAndPassword(user.PassHash, []byte(password)); err != nil { a.log.Info("invalid credentials", sl.Err(err)) return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials) } app, err := a.appProvider.App(ctx, appID) if err != nil { return "", fmt.Errorf("%s: %w", op, err) } log.Info("user logged in successfully") token, err := jwt.NewToken(user, app.ID, app.Secret, a.tokenTTL) if err != nil { a.log.Error("failed to generate token", sl.Err(err)) return "", fmt.Errorf("%s: %w", op, err) } return token, nil } // RegisterNewUser registers new user in the system and returns user ID. // If user with given username already exists, returns error. func (a *Auth) RegisterNewUser(ctx context.Context, email string, pass string) (int64, error) { const op = "services.auth.RegisterNewUser" log := a.log.With( slog.String("op", op), slog.String("email", email), ) log.Info("registering user") passHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) if err != nil { log.Error("failed to generate password hash", sl.Err(err)) return 0, fmt.Errorf("%s: %w", op, err) } id, err := a.usrSaver.SaveUser(ctx, email, passHash) if err != nil { if errors.Is(err, storage.ErrUserExists) { log.Warn("user already exists", sl.Err(err)) return 0, fmt.Errorf("%s: %w", op, ErrUserExists) } log.Error("failed to save user", sl.Err(err)) return 0, fmt.Errorf("%s: %w", op, err) } log.Info("user registred") return id, nil } // IsAdmin checks if user is admin func (a *Auth) IsAdmin(ctx context.Context, userID int64) (bool, error) { const op = "services.auth.IsAdmin" log := a.log.With( slog.String("op", op), slog.Int64("user_id", userID), ) log.Info("checking if user is admin") isAdmin, err := a.usrProvider.IsAdmin(ctx, userID) if err != nil { if errors.Is(err, storage.ErrAppNotFound) { log.Warn("app not found", sl.Err(err)) return false, fmt.Errorf("%s: %w", op, ErrInvalidAppID) } return false, fmt.Errorf("%s: %w", op, err) } log.Info("checking if user is admin", slog.Bool("is_admin", isAdmin)) return isAdmin, nil }