lens @ dd6dc669fa232716cce0e2dbc5c4f7bd32296d66

  1package service
  2
  3import (
  4	"bytes"
  5	"context"
  6	"crypto/aes"
  7	"crypto/cipher"
  8	"crypto/rand"
  9	"encoding/gob"
 10	"errors"
 11	"io"
 12
 13	"golang.org/x/crypto/bcrypt"
 14
 15	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 16)
 17
 18type AuthController struct {
 19	authRepository repository.AuthRepository
 20	userRepository repository.UserRepository
 21	key            []byte
 22}
 23
 24var InvalidLogin = errors.New("Invalid login")
 25
 26func NewAuthController(
 27	authRepository repository.AuthRepository,
 28	userRepository repository.UserRepository,
 29	key []byte,
 30) *AuthController {
 31	return &AuthController{
 32		authRepository: authRepository,
 33		userRepository: userRepository,
 34		key:            key,
 35	}
 36}
 37
 38func (c *AuthController) Login(ctx context.Context, username, password []byte) ([]byte, error) {
 39	id, err := c.authRepository.GetIDByUsername(ctx, string(username))
 40	if errors.Is(err, repository.ErrRecordNotFound) {
 41		return nil, InvalidLogin
 42	} else if err != nil {
 43		return nil, err
 44	}
 45
 46	hashedPassword, err := c.authRepository.GetPassword(ctx, id)
 47	if errors.Is(err, repository.ErrRecordNotFound) {
 48		return nil, InvalidLogin
 49	} else if err != nil {
 50		return nil, err
 51	}
 52
 53	if err := bcrypt.CompareHashAndPassword(hashedPassword, password); err != nil {
 54		return nil, InvalidLogin
 55	}
 56
 57	token := &Token{
 58		UserID:   id,
 59		Username: string(username),
 60	}
 61	return WriteToken(token, c.key)
 62}
 63
 64// InitialRegister register a initial user, it will validate if there is another
 65// user stored already. If so an error `InvlidaInput` will be returned
 66func (c *AuthController) InitialRegister(ctx context.Context, username, password []byte, path []byte) error {
 67	exist, err := c.userRepository.Any(ctx)
 68	if err != nil {
 69		return err
 70	}
 71
 72	if exist {
 73		return InvlidInput
 74	}
 75
 76	hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
 77	if err != nil {
 78		return err
 79	}
 80
 81	_, err = c.userRepository.Create(ctx, &repository.CreateUser{
 82		Username: string(username),
 83		Password: hash,
 84		IsAdmin:  true,
 85		Path:     string(path),
 86	})
 87
 88	return err
 89}
 90
 91func (u *AuthController) List(ctx context.Context) ([]*repository.User, error) {
 92	return u.userRepository.List(ctx)
 93}
 94
 95func (u *AuthController) Get(ctx context.Context, id uint) (*repository.User, error) {
 96	return u.userRepository.Get(ctx, id)
 97}
 98
 99func (u *AuthController) Delete(ctx context.Context, id uint) error {
100	return u.userRepository.Delete(ctx, id)
101}
102
103func (u *AuthController) Upsert(
104	ctx context.Context,
105	id *uint,
106	username string,
107	name string,
108	password []byte,
109	isAdmin bool,
110	path string,
111) error {
112	if id != nil {
113		if err := u.userRepository.Update(ctx, *id, &repository.UpdateUser{
114			Username: username,
115			Name:     name,
116			IsAdmin:  isAdmin,
117			Path:     path,
118		}); err != nil {
119			return err
120		}
121
122		if len(password) > 0 {
123			hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
124			if err != nil {
125				return err
126			}
127
128			return u.userRepository.UpdatePassword(ctx, *id, hash)
129		}
130		return nil
131	}
132
133	hash, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
134	if err != nil {
135		return err
136	}
137
138	_, err = u.userRepository.Create(ctx, &repository.CreateUser{
139		Username: username,
140		Name:     name,
141		Password: hash,
142		IsAdmin:  isAdmin,
143		Path:     path,
144	})
145
146	return err
147}
148
149type (
150	AuthKey string
151	Token   struct {
152		UserID   uint
153		Username string
154	}
155)
156
157const TokenKey AuthKey = "token"
158
159func ReadToken(data []byte, key []byte) (*Token, error) {
160	block, err := aes.NewCipher(key)
161	if err != nil {
162		return nil, err
163	}
164
165	aesgcm, err := cipher.NewGCM(block)
166	if err != nil {
167		panic(err.Error())
168	}
169
170	nonceSize := aesgcm.NonceSize()
171	if len(data) < nonceSize {
172		return nil, errors.New("nonce size greater than data's size")
173	}
174
175	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
176	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
177	if err != nil {
178		return nil, err
179	}
180
181	r := bytes.NewReader(plaintext)
182	var token Token
183	dec := gob.NewDecoder(r)
184	if err = dec.Decode(&token); err != nil {
185		return nil, err
186	}
187	return &token, nil
188}
189
190func WriteToken(token *Token, key []byte) ([]byte, error) {
191	block, err := aes.NewCipher(key)
192	if err != nil {
193		return nil, err
194	}
195
196	aesgcm, err := cipher.NewGCM(block)
197	if err != nil {
198		return nil, err
199	}
200
201	var buffer bytes.Buffer
202	enc := gob.NewEncoder(&buffer)
203	if err := enc.Encode(token); err != nil {
204		return nil, err
205	}
206	nonce := make([]byte, aesgcm.NonceSize())
207	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
208		return nil, err
209	}
210
211	ciphertext := aesgcm.Seal(nonce, nonce, buffer.Bytes(), nil)
212	return ciphertext, nil
213}