Skip to content

Commit cdf25eb

Browse files
committed
[users] separate users package
1 parent 3459610 commit cdf25eb

File tree

7 files changed

+340
-0
lines changed

7 files changed

+340
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package users
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"time"
11+
12+
"github.com/android-sms-gateway/server/pkg/cache"
13+
)
14+
15+
const loginCacheTTL = time.Hour
16+
17+
type loginCacheWrapper struct {
18+
ID string
19+
20+
CreatedAt time.Time
21+
UpdatedAt time.Time
22+
}
23+
24+
func (w *loginCacheWrapper) Unmarshal(data []byte) error {
25+
return json.Unmarshal(data, w)
26+
}
27+
28+
func (w *loginCacheWrapper) Marshal() ([]byte, error) {
29+
return json.Marshal(w)
30+
}
31+
32+
type loginCache struct {
33+
storage *cache.Typed[*loginCacheWrapper]
34+
}
35+
36+
func newLoginCache(storage cache.Cache) *loginCache {
37+
return &loginCache{
38+
storage: cache.NewTyped[*loginCacheWrapper](storage),
39+
}
40+
}
41+
42+
func (c *loginCache) makeKey(username, password string) string {
43+
hash := sha256.Sum256([]byte(username + "\x00" + password))
44+
return hex.EncodeToString(hash[:])
45+
}
46+
47+
func (c *loginCache) Get(ctx context.Context, username, password string) (*User, error) {
48+
user, err := c.storage.Get(ctx, c.makeKey(username, password), cache.AndSetTTL(loginCacheTTL))
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to get user from cache: %w", err)
51+
}
52+
53+
return &User{
54+
ID: user.ID,
55+
CreatedAt: user.CreatedAt,
56+
UpdatedAt: user.UpdatedAt,
57+
}, nil
58+
}
59+
60+
func (c *loginCache) Set(ctx context.Context, username, password string, user User) error {
61+
wrapper := &loginCacheWrapper{
62+
ID: user.ID,
63+
CreatedAt: user.CreatedAt,
64+
UpdatedAt: user.UpdatedAt,
65+
}
66+
67+
if err := c.storage.Set(ctx, c.makeKey(username, password), wrapper, cache.WithTTL(loginCacheTTL)); err != nil {
68+
return fmt.Errorf("failed to cache user: %w", err)
69+
}
70+
71+
return nil
72+
}
73+
74+
func (c *loginCache) Delete(ctx context.Context, username, password string) error {
75+
err := c.storage.Delete(ctx, c.makeKey(username, password))
76+
if err == nil || errors.Is(err, cache.ErrKeyNotFound) || errors.Is(err, cache.ErrKeyExpired) {
77+
return nil
78+
}
79+
80+
return fmt.Errorf("failed to delete user from cache: %w", err)
81+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package users
2+
3+
import "time"
4+
5+
type User struct {
6+
ID string
7+
8+
CreatedAt time.Time
9+
UpdatedAt time.Time
10+
}
11+
12+
func newUser(model *userModel) *User {
13+
return &User{
14+
ID: model.ID,
15+
16+
CreatedAt: model.CreatedAt,
17+
UpdatedAt: model.UpdatedAt,
18+
}
19+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package users
2+
3+
import "errors"
4+
5+
var (
6+
ErrNotFound = errors.New("user not found")
7+
ErrExists = errors.New("user already exists")
8+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package users
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
7+
"gorm.io/gorm"
8+
)
9+
10+
type userModel struct {
11+
ID string `gorm:"primaryKey;type:varchar(32)"`
12+
PasswordHash string `gorm:"not null;type:varchar(72)"`
13+
// Devices []models.Device `gorm:"-,foreignKey:UserID;constraint:OnDelete:CASCADE"`
14+
15+
models.SoftDeletableModel
16+
}
17+
18+
func (u *userModel) TableName() string {
19+
return "users"
20+
}
21+
22+
func Migrate(db *gorm.DB) error {
23+
if err := db.AutoMigrate(new(userModel)); err != nil {
24+
return fmt.Errorf("users migration failed: %w", err)
25+
}
26+
return nil
27+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package users
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/android-sms-gateway/server/internal/sms-gateway/cache"
7+
"github.com/capcom6/go-infra-fx/db"
8+
"github.com/go-core-fx/logger"
9+
"go.uber.org/fx"
10+
)
11+
12+
func Module() fx.Option {
13+
return fx.Module(
14+
"users",
15+
logger.WithNamedLogger("users"),
16+
fx.Provide(func(factory cache.Factory) (*loginCache, error) {
17+
storage, err := factory.New("users:login")
18+
if err != nil {
19+
return nil, fmt.Errorf("can't create login cache: %w", err)
20+
}
21+
22+
return newLoginCache(storage), nil
23+
}, fx.Private),
24+
fx.Provide(newRepository, fx.Private),
25+
fx.Provide(NewService),
26+
)
27+
}
28+
29+
func init() {
30+
db.RegisterMigration(Migrate)
31+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package users
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"gorm.io/gorm"
8+
)
9+
10+
type repository struct {
11+
db *gorm.DB
12+
}
13+
14+
// newRepository creates a new repository instance.
15+
func newRepository(db *gorm.DB) *repository {
16+
return &repository{
17+
db: db,
18+
}
19+
}
20+
21+
func (r *repository) Exists(id string) (bool, error) {
22+
var count int64
23+
if err := r.db.Model((*userModel)(nil)).
24+
Where("id = ?", id).
25+
Count(&count).Error; err != nil {
26+
return false, fmt.Errorf("can't check if user exists: %w", err)
27+
}
28+
29+
return count > 0, nil
30+
}
31+
32+
// GetByID retrieves a user by their ID.
33+
func (r *repository) GetByID(id string) (*userModel, error) {
34+
user := new(userModel)
35+
36+
if err := r.db.Where("id = ?", id).Take(&user).Error; err != nil {
37+
if errors.Is(err, gorm.ErrRecordNotFound) {
38+
return nil, ErrNotFound
39+
}
40+
return nil, fmt.Errorf("can't get user: %w", err)
41+
}
42+
43+
return user, nil
44+
}
45+
46+
func (r *repository) Insert(user *userModel) error {
47+
if err := r.db.Create(user).Error; err != nil {
48+
return fmt.Errorf("can't create user: %w", err)
49+
}
50+
51+
return nil
52+
}
53+
54+
func (r *repository) UpdatePassword(ID string, passwordHash string) error {
55+
if err := r.db.Model((*userModel)(nil)).
56+
Where("id = ?", ID).
57+
Update("password_hash", passwordHash).Error; err != nil {
58+
return fmt.Errorf("can't update password: %w", err)
59+
}
60+
61+
return nil
62+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package users
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/android-sms-gateway/server/pkg/cache"
9+
"github.com/android-sms-gateway/server/pkg/crypto"
10+
"go.uber.org/zap"
11+
)
12+
13+
type Service struct {
14+
users *repository
15+
16+
cache *loginCache
17+
18+
logger *zap.Logger
19+
}
20+
21+
func NewService(
22+
users *repository,
23+
cache *loginCache,
24+
logger *zap.Logger,
25+
) *Service {
26+
return &Service{
27+
users: users,
28+
29+
cache: cache,
30+
31+
logger: logger,
32+
}
33+
}
34+
35+
func (s *Service) Create(username, password string) (*User, error) {
36+
exists, err := s.users.Exists(username)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
if exists {
42+
return nil, fmt.Errorf("%w: %s", ErrExists, username)
43+
}
44+
45+
passwordHash, err := crypto.MakeBCryptHash(password)
46+
if err != nil {
47+
return nil, fmt.Errorf("failed to hash password: %w", err)
48+
}
49+
50+
user := &userModel{
51+
ID: username,
52+
PasswordHash: passwordHash,
53+
}
54+
55+
if err := s.users.Insert(user); err != nil {
56+
return nil, fmt.Errorf("failed to create user: %w", err)
57+
}
58+
59+
return newUser(user), nil
60+
}
61+
62+
func (s *Service) GetByUsername(username string) (*User, error) {
63+
user, err := s.users.GetByID(username)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
return newUser(user), nil
69+
}
70+
71+
func (s *Service) Login(ctx context.Context, username, password string) (*User, error) {
72+
cachedUser, err := s.cache.Get(ctx, username, password)
73+
if err == nil {
74+
return cachedUser, nil
75+
} else if !errors.Is(err, cache.ErrKeyNotFound) {
76+
s.logger.Warn("failed to get user from cache", zap.String("username", username), zap.Error(err))
77+
}
78+
79+
user, err := s.users.GetByID(username)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
if err := crypto.CompareBCryptHash(user.PasswordHash, password); err != nil {
85+
return nil, fmt.Errorf("login failed: %w", err)
86+
}
87+
88+
loggedInUser := newUser(user)
89+
if err := s.cache.Set(ctx, username, password, *loggedInUser); err != nil {
90+
s.logger.Error("failed to cache user", zap.String("username", username), zap.Error(err))
91+
}
92+
93+
return loggedInUser, nil
94+
}
95+
96+
func (s *Service) ChangePassword(ctx context.Context, username, currentPassword, newPassword string) error {
97+
_, err := s.Login(ctx, username, currentPassword)
98+
if err != nil {
99+
return err
100+
}
101+
102+
if err := s.cache.Delete(ctx, username, currentPassword); err != nil {
103+
return err
104+
}
105+
106+
passwordHash, err := crypto.MakeBCryptHash(newPassword)
107+
if err != nil {
108+
return fmt.Errorf("failed to hash password: %w", err)
109+
}
110+
111+
return s.users.UpdatePassword(username, passwordHash)
112+
}

0 commit comments

Comments
 (0)