2024-11-18 15:23:13 +00:00
|
|
|
// 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.
|
|
|
|
|
2024-11-17 16:58:29 +00:00
|
|
|
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"
|
2024-11-19 05:10:30 +00:00
|
|
|
"gitserver.in/patialtech/rano/util/uid"
|
2024-11-17 16:58:29 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
|
|
|
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 {
|
2024-11-19 05:10:30 +00:00
|
|
|
return context.WithValue(ctx, config.AuthUserCtxKey, &AuthUser{
|
2024-11-17 16:58:29 +00:00
|
|
|
ID: u.ID,
|
|
|
|
Email: u.Email,
|
|
|
|
Name: u.Name,
|
|
|
|
RoleID: u.RoleID,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-11-19 05:10:30 +00:00
|
|
|
func CtxUser(ctx context.Context) *AuthUser {
|
|
|
|
u, _ := ctx.Value(config.AuthUserCtxKey).(*AuthUser)
|
2024-11-17 16:58:29 +00:00
|
|
|
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()
|
2024-11-19 05:10:30 +00:00
|
|
|
// user IP
|
|
|
|
ip, _ := ctx.Value("RequestIP").(string)
|
|
|
|
// user Agent
|
|
|
|
ua, _ := ctx.Value("RequestUA").(string)
|
2024-11-17 16:58:29 +00:00
|
|
|
|
2024-11-19 05:10:30 +00:00
|
|
|
// create session entry in db
|
|
|
|
s, err := db.Client().UserSession.Create().
|
2024-11-17 16:58:29 +00:00
|
|
|
SetUserID(u.ID).
|
|
|
|
SetIssuedAt(time.Now().UTC()).
|
|
|
|
SetExpiresAt(until).
|
2024-11-19 05:10:30 +00:00
|
|
|
SetIP(ip).
|
|
|
|
SetUserAgent(ua).
|
|
|
|
Save(ctx)
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
return nil, ErrUnexpected
|
|
|
|
}
|
|
|
|
|
|
|
|
sid, err := uid.Encode(
|
|
|
|
uint64(u.ID),
|
|
|
|
uint64(s.ID),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
return nil, ErrUnexpected
|
|
|
|
}
|
2024-11-17 16:58:29 +00:00
|
|
|
|
|
|
|
return &AuthUser{
|
2024-11-19 05:10:30 +00:00
|
|
|
ID: sid,
|
2024-11-17 16:58:29 +00:00
|
|
|
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
|
|
|
|
}
|