/
app.go
144 lines (123 loc) · 3.46 KB
/
app.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package app
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"time"
"github.com/benjohns1/blinkfile"
)
type (
Config struct {
Log
AdminUsername string
AdminPassword string
SessionExpiration time.Duration
SessionRepo
FileRepo
UserRepo
CredentialRepo
GenerateToken func() (Token, error)
Clock
PasswordHasher
GenerateFileID func() (blinkfile.FileID, error)
GenerateUserID func() (blinkfile.UserID, error)
}
SessionRepo interface {
Save(context.Context, Session) error
Get(context.Context, Token) (Session, bool, error)
Delete(context.Context, Token) error
DeleteAllUserSessions(context.Context, blinkfile.UserID) (int, error)
}
FileRepo interface {
Save(context.Context, blinkfile.File) error
ListByUser(context.Context, blinkfile.UserID) ([]blinkfile.FileHeader, error)
DeleteExpiredBefore(context.Context, time.Time) (int, error)
Get(context.Context, blinkfile.FileID) (blinkfile.FileHeader, error)
Delete(context.Context, blinkfile.UserID, []blinkfile.FileID) error
PutHeader(context.Context, blinkfile.FileHeader) error
}
UserRepo interface {
Create(context.Context, blinkfile.User) error
Get(context.Context, blinkfile.UserID) (blinkfile.User, bool, error)
ListAll(context.Context) ([]blinkfile.User, error)
Delete(context.Context, blinkfile.UserID) error
}
CredentialRepo interface {
Set(context.Context, Credentials) error
GetByUsername(context.Context, blinkfile.Username) (Credentials, error)
Remove(context.Context, blinkfile.UserID) error
}
PasswordHasher interface {
Hash(data []byte) (hash string)
Match(hash string, data []byte) (matched bool, err error)
}
App struct {
cfg Config
adminCredentials map[blinkfile.Username]Credentials
Log
}
Log interface {
Printf(ctx context.Context, format string, v ...any)
Errorf(ctx context.Context, format string, v ...any)
}
Clock interface {
Now() time.Time
}
)
var FeatureFlagIsOn = func(context.Context, string) bool { return false }
type DefaultClock struct{}
func (c *DefaultClock) Now() time.Time {
return time.Now().UTC()
}
func New(ctx context.Context, cfg Config) (*App, error) {
if cfg.Log == nil {
return nil, fmt.Errorf("log instance is required")
}
if cfg.Clock == nil {
cfg.Clock = &DefaultClock{}
}
if cfg.GenerateToken == nil {
cfg.GenerateToken = generateDefaultToken
}
if cfg.SessionRepo == nil {
return nil, fmt.Errorf("session repo is required")
}
if cfg.FileRepo == nil {
return nil, fmt.Errorf("file repo is required")
}
if cfg.PasswordHasher == nil {
return nil, fmt.Errorf("password hasher is required")
}
if cfg.GenerateFileID == nil {
cfg.GenerateFileID = generateFileID
}
if cfg.GenerateUserID == nil {
cfg.GenerateUserID = generateUserID
}
a := &App{cfg, make(map[blinkfile.Username]Credentials, 1), cfg.Log}
err := a.registerAdminUser(ctx, blinkfile.Username(cfg.AdminUsername), cfg.AdminPassword)
if err != nil {
return nil, err
}
return a, nil
}
func generateFileID() (blinkfile.FileID, error) {
const fileIDLength = 64
id, err := generateRandomBase64(fileIDLength)
return blinkfile.FileID(id), err
}
func generateUserID() (blinkfile.UserID, error) {
const userIDLength = 32
id, err := generateRandomBase64(userIDLength)
return blinkfile.UserID(id), err
}
func generateRandomBase64(length int) (string, error) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return "", err
}
id := base64.URLEncoding.EncodeToString(b)
return id, nil
}