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}