// Copyright 2024 Patial Tech. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package user import ( "context" "errors" "log/slog" "time" "gitserver.in/patialtech/rano/config" "gitserver.in/patialtech/rano/db" "gitserver.in/patialtech/rano/db/ent" "gitserver.in/patialtech/rano/db/ent/user" "gitserver.in/patialtech/rano/graph/model" "gitserver.in/patialtech/rano/util/crypto" "gitserver.in/patialtech/rano/util/logger" ) type ( SessionUser struct { ID string Email string Name string RoleID int } AuthUser = model.AuthUser ) var ( ErrInvalidCred = errors.New("invalid email or password") ErrAccountNotActive = errors.New("account is not active") ErrAccountLocked = errors.New("account is locked, please try after sometime") ErrUnexpected = errors.New("unexpected error has happened") ) func CtxWithUser(ctx context.Context, u *AuthUser) context.Context { return context.WithValue(ctx, config.AuthUserCtxKey, &SessionUser{ ID: u.ID, Email: u.Email, Name: u.Name, RoleID: u.RoleID, }) } func CtxUser(ctx context.Context) *SessionUser { u, _ := ctx.Value(config.AuthUserCtxKey).(*SessionUser) return u } // NewSession for user. // // Authenticated func NewSession(ctx context.Context, email, pwd string) (*AuthUser, error) { // authenticate. u, err := authenticate(ctx, email, pwd) if err != nil { return nil, err } // 30 day token life until := time.Now().Add(time.Hour * 24 * 30).UTC() // create sesion entry in db db.Client().UserSession.Create(). SetUserID(u.ID). SetIssuedAt(time.Now().UTC()). SetExpiresAt(until). SetIP(""). SetUserAgent("") return &AuthUser{ Name: fullName(u.FirstName, *u.MiddleName, u.LastName), }, nil } // RemoveSession entry from DB func RemoveSession(sID uint) { panic("not implemented") } // authenticate user against DB func authenticate(ctx context.Context, email, pwd string) (*ent.User, error) { client := db.Client() // incident email attr attrEmail := slog.String("email", email) // get user by given email u, err := client.User. Query(). Where(user.EmailEQ(email)). Select( user.FieldEmail, user.FieldPwdHash, user.FieldPwdSalt, user.FieldLoginFailedCount, user.FieldLoginLockedUntil, user.FieldLoginAttemptOn, user.FieldFirstName, user.FieldMiddleName, user.FieldLastName, user.FieldStatus, ). Only(ctx) if err != nil { if ent.IsNotFound(err) { logger.Incident(ctx, "Authenticate", "wrong email", attrEmail) return nil, ErrInvalidCred } logger.Error(err) return nil, ErrUnexpected } // check account is ready for authentication // ensure that user account is active or perform other needed checks if u.Status != user.StatusActive { logger.Incident(ctx, "Authenticate", "account issue", attrEmail) return nil, ErrAccountNotActive } // check account is locked lck := u.LoginLockedUntil now := time.Now().UTC() if lck != nil && now.Before(lck.UTC()) { logger.Incident(ctx, "Authenticate", "account locked", attrEmail) return nil, ErrAccountLocked } upQry := client.User.UpdateOneID(u.ID) // compare password // in-case password is wrong, lets increment failed attempt if !crypto.ComparePasswordHash(pwd, u.PwdHash, u.PwdSalt) { var locked bool u.LoginFailedCount++ upQry. SetLoginAttemptOn(time.Now().UTC()). SetLoginFailedCount(u.LoginFailedCount) // lock user if count is more that 4 if u.LoginFailedCount > 4 { locked = true upQry.SetLoginLockedUntil(time.Now().Add(time.Hour * 6).UTC()) } // update user login attempt status if err = upQry.Exec(ctx); err != nil { return nil, ErrUnexpected } if locked { return nil, ErrAccountLocked } return nil, ErrInvalidCred } if u.LoginFailedCount > 0 { u.LoginFailedCount = 0 upQry.ClearLoginFailedCount() if err := upQry.Exec(ctx); err != nil { logger.Error(err, attrEmail) } } // let's not get them out u.PwdHash = "" u.PwdSalt = "" return u, nil }