package store import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "time" "golang.org/x/crypto/bcrypt" ) type User struct { ID int64 `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Username string `json:"username"` Email string `json:"email"` Password password `json:"-"` IsActive bool `json:"is_active"` RoleID int64 `json:"role_id"` Role Role `json:"role"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type password struct { text *string `validate:"required,password"` hash []byte } func (p *password) Set(password string) error { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } p.text = &password p.hash = hash return nil } type UserStore struct { db *sql.DB } func (s *UserStore) Create(ctx context.Context, tx *sql.Tx, user *User) error { query := ` INSERT INTO users (first_name, last_name, username, email, password, role_id) VALUES ($1, $2, $3, $4, $5, (SELECT id FROM roles WHERE name = $6)) RETURNING id, created_at, updated_at ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() role := user.Role.Name if role == "" { role = "user" } err := tx.QueryRowContext( ctx, query, user.FirstName, user.LastName, user.Username, user.Email, user.Password.hash, role, ).Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt) if err != nil { switch { case err.Error() == `pq: duplicate key value violates unique constraint "users_username_key"`: return ErrDuplicateUsername case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`: return ErrDuplicateEmail default: return err } } return nil } func (s *UserStore) GetByID(ctx context.Context, id int64) (*User, error) { query := ` SELECT users.id, first_name, last_name, username, email, password, created_at, updated_at, roles.* FROM users JOIN roles ON (users.role_id = roles.id) WHERE users.id = $1 AND is_active = true ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() user := &User{} err := s.db.QueryRowContext(ctx, query, id).Scan( &user.ID, &user.FirstName, &user.LastName, &user.Username, &user.Email, &user.Password.hash, &user.CreatedAt, &user.UpdatedAt, &user.Role.ID, &user.Role.Name, &user.Role.Level, &user.Role.Description, ) if err != nil { switch err { case sql.ErrNoRows: return nil, ErrNotFound default: return nil, err } } return user, nil } func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error) { query := ` SELECT users.id, first_name, last_name, username, email, password, created_at, updated_at, roles.* FROM users JOIN roles ON (users.role_id = roles.id) WHERE email = $1 AND is_active = true ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() user := &User{} err := s.db.QueryRowContext(ctx, query, email).Scan( &user.ID, &user.FirstName, &user.LastName, &user.Username, &user.Email, &user.Password.hash, &user.CreatedAt, &user.UpdatedAt, &user.Role.ID, &user.Role.Name, &user.Role.Level, &user.Role.Description, ) if err != nil { switch err { case sql.ErrNoRows: return nil, ErrNotFound default: return nil, err } } return user, nil } func (s *UserStore) Update(ctx context.Context, user *User) error { query := ` UPDATE users SET first_name = $1, last_name = $2, username = $3, email = $4, password = $5 WHERE id = $6 ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() _, err := s.db.ExecContext( ctx, query, user.FirstName, user.LastName, user.Username, user.Email, user.Password.hash, user.ID, ) if err != nil { return err } return nil } func (s *UserStore) CreateAndInvite(ctx context.Context, user *User, token string, invitationExp time.Duration) error { return withTx(s.db, ctx, func(tx *sql.Tx) error { if err := s.Create(ctx, tx, user); err != nil { return err } if err := s.createUserInvitation(ctx, tx, user.ID, invitationExp, token); err != nil { return err } return nil }) } func (s *UserStore) Activate(ctx context.Context, token string) error { return withTx(s.db, ctx, func(tx *sql.Tx) error { // Find the user that this token belongs to user, err := getUserFromInvitation(ctx, tx, token) if err != nil { return err } // Update the user's status to active user.IsActive = true if err := s.update(ctx, tx, user); err != nil { return err } // Delete the token from the invitations table if err := s.deleteUserInvitation(ctx, tx, user.ID); err != nil { return err } return nil }) } func (s *UserStore) Delete(ctx context.Context, id int64) error { return withTx(s.db, ctx, func(tx *sql.Tx) error { if err := s.delete(ctx, tx, id); err != nil { return err } if err := s.deleteUserInvitation(ctx, tx, id); err != nil { return err } return nil }) } // * Soft delete user func (s *UserStore) DeleteUser(ctx context.Context, id int64) error { query := `UPDATE users SET is_active = false WHERE id = $1` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() _, err := s.db.ExecContext(ctx, query, id) if err != nil { return err } return nil } func (p *password) Compare(password string) error { return bcrypt.CompareHashAndPassword(p.hash, []byte(password)) } func getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User, error) { query := ` SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.created_at, u.updated_at, is_active FROM users u JOIN invitations i ON u.id = i.user_id WHERE i.token = $1 AND i.expiry > $2 ` hashed := sha256.Sum256([]byte(token)) hashedToken := hex.EncodeToString(hashed[:]) ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() user := &User{} err := tx.QueryRowContext(ctx, query, hashedToken, time.Now()).Scan( &user.ID, &user.FirstName, &user.LastName, &user.Username, &user.Email, &user.CreatedAt, &user.UpdatedAt, &user.IsActive, ) if err != nil { switch err { case sql.ErrNoRows: return nil, ErrNotFound default: return nil, err } } return user, nil } func (s *UserStore) createUserInvitation(ctx context.Context, tx *sql.Tx, userID int64, exp time.Duration, token string) error { query := ` INSERT INTO invitations (user_id, token, expiry) VALUES ($1, $2, $3) ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() _, err := tx.ExecContext(ctx, query, userID, token, time.Now().Add(exp)) if err != nil { return err } return nil } func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error { query := ` UPDATE users SET first_name = $1, last_name = $2, username = $3, email = $4, is_active = $5 WHERE id = $6 ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() _, err := tx.ExecContext( ctx, query, user.FirstName, user.LastName, user.Username, user.Email, user.IsActive, user.ID, ) if err != nil { return err } return nil } func (s *UserStore) deleteUserInvitation(ctx context.Context, tx *sql.Tx, userID int64) error { query := `DELETE FROM invitations WHERE user_id = $1` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() _, err := tx.ExecContext(ctx, query, userID) if err != nil { return err } return nil } func (s *UserStore) delete(ctx context.Context, tx *sql.Tx, id int64) error { query := `DELETE FROM users WHERE id = $1` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() _, err := tx.ExecContext(ctx, query, id) if err != nil { return err } return nil }