rano/pkg/user/session.go
2024-11-18 20:53:13 +05:30

168 lines
4.0 KiB
Go

// 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
}