lens @ master

  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	Token struct {
151		UserID   uint
152		Username string
153	}
154)
155
156func ReadToken(data []byte, key []byte) (*Token, error) {
157	block, err := aes.NewCipher(key)
158	if err != nil {
159		return nil, err
160	}
161
162	aesgcm, err := cipher.NewGCM(block)
163	if err != nil {
164		panic(err.Error())
165	}
166
167	nonceSize := aesgcm.NonceSize()
168	if len(data) < nonceSize {
169		return nil, errors.New("nonce size greater than data's size")
170	}
171
172	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
173	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
174	if err != nil {
175		return nil, err
176	}
177
178	r := bytes.NewReader(plaintext)
179	var token Token
180	dec := gob.NewDecoder(r)
181	if err = dec.Decode(&token); err != nil {
182		return nil, err
183	}
184	return &token, nil
185}
186
187func WriteToken(token *Token, key []byte) ([]byte, error) {
188	block, err := aes.NewCipher(key)
189	if err != nil {
190		return nil, err
191	}
192
193	aesgcm, err := cipher.NewGCM(block)
194	if err != nil {
195		return nil, err
196	}
197
198	var buffer bytes.Buffer
199	enc := gob.NewEncoder(&buffer)
200	if err := enc.Encode(token); err != nil {
201		return nil, err
202	}
203	nonce := make([]byte, aesgcm.NonceSize())
204	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
205		return nil, err
206	}
207
208	ciphertext := aesgcm.Seal(nonce, nonce, buffer.Bytes(), nil)
209	return ciphertext, nil
210}