364 lines
7.7 KiB
Go
364 lines
7.7 KiB
Go
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
|
|
}
|