Add login/register logic

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-25 17:32:26 +01:00
parent 49c3807b4f
commit e9ae77a4a0
28 changed files with 2075 additions and 419 deletions

View File

@@ -0,0 +1,5 @@
-- Add organization_id column to usrmgr_users table
ALTER TABLE usrmgr_users ADD COLUMN organization_id TEXT REFERENCES organizations(id);
-- Create an index for faster lookups
CREATE INDEX usrmgr_users_organization_id_idx ON usrmgr_users(organization_id);

View File

@@ -16,7 +16,6 @@ package coredata
import (
"context"
"fmt"
"time"
"github.com/getprobo/probo/pkg/gid"
@@ -57,17 +56,17 @@ func (s *Session) LoadByID(
q := `
SELECT
id,
user_id,
expired_at,
created_at,
updated_at
FROM
sessions
usrmgr_sessions
WHERE
id = @session_id
LIMIT 1;
`
q = fmt.Sprintf(q)
args := pgx.NamedArgs{"session_id": sessionID}
r := conn.QueryRow(ctx, q, args)
@@ -88,7 +87,7 @@ func (s *Session) Insert(
) error {
q := `
INSERT INTO
sessions (id, user_id, expired_at, created_at, updated_at)
usrmgr_sessions (id, user_id, expired_at, created_at, updated_at)
VALUES (
@session_id,
@user_id,
@@ -109,3 +108,44 @@ VALUES (
_, err := conn.Exec(ctx, q, args)
return err
}
func (s *Session) Update(
ctx context.Context,
conn pg.Conn,
) error {
q := `
UPDATE usrmgr_sessions
SET
expired_at = @expired_at,
updated_at = @updated_at
WHERE
id = @session_id
`
args := pgx.NamedArgs{
"session_id": s.ID,
"expired_at": s.ExpiredAt,
"updated_at": s.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
}
func DeleteSession(
ctx context.Context,
conn pg.Conn,
sessionID gid.GID,
) error {
q := `
DELETE FROM
usrmgr_sessions
WHERE
id = @session_id
`
args := pgx.NamedArgs{"session_id": sessionID}
_, err := conn.Exec(ctx, q, args)
return err
}

View File

@@ -29,6 +29,7 @@ type (
ID gid.GID
EmailAddress string
HashedPassword []byte
OrganizationID gid.GID
CreatedAt time.Time
UpdatedAt time.Time
}
@@ -43,6 +44,7 @@ func (u *User) scan(r pgx.Row) error {
&u.ID,
&u.EmailAddress,
&u.HashedPassword,
&u.OrganizationID,
&u.CreatedAt,
&u.UpdatedAt,
)
@@ -58,12 +60,13 @@ SELECT
id,
email_address,
hashed_password,
organization_id,
created_at,
updated_at
FROM
users
usrmgr_users
WHERE
email = @user_email
email_address = @user_email
LIMIT 1;
`
@@ -80,3 +83,67 @@ LIMIT 1;
return nil
}
func (u *User) LoadByID(
ctx context.Context,
conn pg.Conn,
userID gid.GID,
) error {
q := `
SELECT
id,
email_address,
hashed_password,
organization_id,
created_at,
updated_at
FROM
usrmgr_users
WHERE
id = @user_id
LIMIT 1;
`
args := pgx.NamedArgs{"user_id": userID}
r := conn.QueryRow(ctx, q, args)
u2 := User{}
if err := u2.scan(r); err != nil {
return err
}
*u = u2
return nil
}
func (u *User) Insert(
ctx context.Context,
conn pg.Conn,
) error {
q := `
INSERT INTO
usrmgr_users (id, email_address, hashed_password, organization_id, created_at, updated_at)
VALUES (
@user_id,
@email_address,
@hashed_password,
@organization_id,
@created_at,
@updated_at
)
`
args := pgx.NamedArgs{
"user_id": u.ID,
"email_address": u.EmailAddress,
"hashed_password": u.HashedPassword,
"organization_id": "AZSfP_xAcAC5IAAAAAAltA",
"created_at": u.CreatedAt,
"updated_at": u.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/usrmgr/coredata"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/migrator"
"go.gearno.de/kit/pg"
)
@@ -30,22 +31,116 @@ type (
pg *pg.Client
hp *HashingProfile
}
RegisterUserParams struct {
Email string
Password string
}
ErrInvalidCredentials struct {
message string
}
ErrUserAlreadyExists struct {
message string
}
ErrSessionNotFound struct {
message string
}
ErrSessionExpired struct {
message string
}
)
func (e ErrInvalidCredentials) Error() string {
return e.message
}
func (e ErrUserAlreadyExists) Error() string {
return e.message
}
func (e ErrSessionNotFound) Error() string {
return e.message
}
func (e ErrSessionExpired) Error() string {
return e.message
}
func NewService(
ctx context.Context,
pgClient *pg.Client,
pepper []byte,
) (*Service, error) {
err := migrator.NewMigrator(pgClient, coredata.Migrations).Run(ctx, "migrations")
if err != nil {
return nil, fmt.Errorf("cannot migrate database schema: %w", err)
}
hp, err := NewHashingProfile(pepper)
if err != nil {
return nil, fmt.Errorf("cannot create hashing profile: %w", err)
}
return &Service{
pg: pgClient,
hp: hp,
}, nil
}
func (s Service) RegisterUser(
ctx context.Context,
params RegisterUserParams,
) (*coredata.User, error) {
if params.Email == "" || params.Password == "" {
return nil, fmt.Errorf("email and password are required")
}
// Use a high iteration count for password hashing
const iterations = 600000
hashedPassword, err := s.hp.HashPassword([]byte(params.Password), iterations)
if err != nil {
return nil, fmt.Errorf("cannot hash password: %w", err)
}
now := time.Now()
user := &coredata.User{
ID: gid.New(),
EmailAddress: params.Email,
HashedPassword: hashedPassword,
CreatedAt: now,
UpdatedAt: now,
}
err = s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
// Check if user already exists
existingUser := &coredata.User{}
err := existingUser.LoadByEmail(ctx, tx, params.Email)
if err == nil {
return &ErrUserAlreadyExists{message: "user with this email already exists"}
}
// Insert the new user
if err := user.Insert(ctx, tx); err != nil {
return fmt.Errorf("cannot insert user: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return user, nil
}
func (s Service) Login(
ctx context.Context,
email string,
@@ -54,8 +149,8 @@ func (s Service) Login(
now := time.Now()
user := &coredata.User{}
session := &coredata.Session{
ID: gid.GID{},
UserID: user.ID,
ID: gid.New(),
UserID: gid.GID{}, // Will be set after user is loaded
ExpiredAt: now.Add(24 * time.Hour),
CreatedAt: now,
UpdatedAt: now,
@@ -65,18 +160,21 @@ func (s Service) Login(
ctx,
func(tx pg.Conn) error {
if err := user.LoadByEmail(ctx, tx, email); err != nil {
return fmt.Errorf("cannot load user by email: %w", err)
return &ErrInvalidCredentials{message: "invalid email or password"}
}
ok, err := s.hp.ComparePasswordAndHash([]byte(password), user.HashedPassword)
if err != nil {
return fmt.Errorf("cannot constant compare byte: %w", err)
return fmt.Errorf("cannot compare password: %w", err)
}
if !ok {
return fmt.Errorf("invalid password")
return &ErrInvalidCredentials{message: "invalid email or password"}
}
// Set the user ID in the session
session.UserID = user.ID
if err := session.Insert(ctx, tx); err != nil {
return fmt.Errorf("cannot insert session: %w", err)
}
@@ -89,13 +187,176 @@ func (s Service) Login(
return nil, err
}
return nil, nil
return session, nil
}
func (s Service) Logout(sessionID string) error {
return nil
func (s Service) Logout(
ctx context.Context,
sessionID gid.GID,
) error {
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
return coredata.DeleteSession(ctx, tx, sessionID)
},
)
}
func (s Service) GetSession(sessionID string) (*coredata.Session, error) {
return nil, nil
func (s Service) GetSession(
ctx context.Context,
sessionID gid.GID,
) (*coredata.Session, error) {
session := &coredata.Session{}
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
if err := session.LoadByID(ctx, tx, sessionID); err != nil {
return &ErrSessionNotFound{message: "session not found"}
}
// Check if session is expired
if time.Now().After(session.ExpiredAt) {
// Delete expired session
if err := coredata.DeleteSession(ctx, tx, sessionID); err != nil {
return fmt.Errorf("cannot delete expired session: %w", err)
}
return &ErrSessionExpired{message: "session expired"}
}
return nil
},
)
if err != nil {
return nil, err
}
return session, nil
}
func (s Service) RefreshSession(
ctx context.Context,
sessionID gid.GID,
) (*coredata.Session, error) {
session := &coredata.Session{}
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
if err := session.LoadByID(ctx, tx, sessionID); err != nil {
return &ErrSessionNotFound{message: "session not found"}
}
// Check if session is expired
if time.Now().After(session.ExpiredAt) {
return &ErrSessionExpired{message: "session expired"}
}
// Update session expiration
now := time.Now()
session.ExpiredAt = now.Add(24 * time.Hour)
session.UpdatedAt = now
if err := session.Update(ctx, tx); err != nil {
return fmt.Errorf("cannot update session: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return session, nil
}
func (s Service) GetUserByID(
ctx context.Context,
userID gid.GID,
) (*coredata.User, error) {
user := &coredata.User{}
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
if err := user.LoadByID(ctx, tx, userID); err != nil {
return fmt.Errorf("user not found: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return user, nil
}
func (s Service) GetUserBySession(
ctx context.Context,
sessionID gid.GID,
) (*coredata.User, error) {
session, err := s.GetSession(ctx, sessionID)
if err != nil {
return nil, err
}
return s.GetUserByID(ctx, session.UserID)
}
// SetUserOrganization sets the organization for a user
func (s Service) SetUserOrganization(
ctx context.Context,
userID gid.GID,
organizationID gid.GID,
) error {
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
user := &coredata.User{}
if err := user.LoadByID(ctx, tx, userID); err != nil {
return fmt.Errorf("user not found: %w", err)
}
// Update the organization ID
user.OrganizationID = organizationID
user.UpdatedAt = time.Now()
// Update the user in the database
q := `
UPDATE usrmgr_users
SET organization_id = @organization_id, updated_at = @updated_at
WHERE id = @user_id
`
args := pgx.NamedArgs{
"user_id": user.ID,
"organization_id": user.OrganizationID,
"updated_at": user.UpdatedAt,
}
_, err := tx.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot update user organization: %w", err)
}
return nil
},
)
}
// GetUserOrganization gets the organization ID for a user
func (s Service) GetUserOrganization(
ctx context.Context,
userID gid.GID,
) (gid.GID, error) {
user, err := s.GetUserByID(ctx, userID)
if err != nil {
return gid.GID{}, err
}
return user.OrganizationID, nil
}