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}