forked from SkynetLabs/skynet-accounts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
286 lines (267 loc) · 10.2 KB
/
main.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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package main
import (
"context"
"fmt"
"log"
"net/url"
"os"
"strconv"
"strings"
"github.com/SkynetLabs/skynet-accounts/api"
"github.com/SkynetLabs/skynet-accounts/build"
"github.com/SkynetLabs/skynet-accounts/database"
"github.com/SkynetLabs/skynet-accounts/email"
"github.com/SkynetLabs/skynet-accounts/jwt"
"github.com/SkynetLabs/skynet-accounts/metafetcher"
"github.com/joho/godotenv"
"github.com/stripe/stripe-go/v72"
"gitlab.com/SkynetLabs/skyd/skymodules"
"github.com/sirupsen/logrus"
"gitlab.com/NebulousLabs/errors"
)
const (
// envAccountsJWKSFile holds the name of the environment variable which
// holds the path to the JWKS file we need to use. Optional.
envAccountsJWKSFile = "ACCOUNTS_JWKS_FILE"
// envJWTTTL holds the name of the environment variable for JWT TTL.
envJWTTTL = "ACCOUNTS_JWT_TTL"
// envDBHost holds the name of the environment variable for DB host.
envDBHost = "SKYNET_DB_HOST"
// envDBPort holds the name of the environment variable for DB port.
envDBPort = "SKYNET_DB_PORT"
// envDBUser holds the name of the environment variable for DB username.
envDBUser = "SKYNET_DB_USER"
// envDBPass holds the name of the environment variable for DB password.
envDBPass = "SKYNET_DB_PASS" // #nosec G101: Potential hardcoded credentials
// envEmailFrom holds the name of the environment variable that allows us to
// override the "from" address of our emails to users.
envEmailFrom = "ACCOUNTS_EMAIL_FROM"
// envEmailURI holds the name of the environment variable for email URI.
envEmailURI = "ACCOUNTS_EMAIL_URI"
// envLogLevel holds the name of the environment variable which defines the
// desired log level.
envLogLevel = "SKYNET_ACCOUNTS_LOG_LEVEL"
// envPortal holds the name of the environment variable for the portal to
// use to fetch skylinks and sign JWT tokens.
envPortal = "PORTAL_DOMAIN"
// envPromoter holds the name of the environment variable which controls
// how premium accounts will be managed. Defaults to 'stripe'.
// Example 1: ACCOUNTS_PROMOTER=stripe
// Example 2: ACCOUNTS_PROMOTER=promoter
envPromoter = "ACCOUNTS_PROMOTER"
// envServerDomain holds the name of the environment variable for the
// identity of this server. Example: eu-ger-1.siasky.net
envServerDomain = "SERVER_DOMAIN"
// envStripeAPIKey hold the name of the environment variable for Stripe's
// API key. It's only required when integrating with Stripe.
envStripeAPIKey = "STRIPE_API_KEY" // #nosec
// envMaxNumAPIKeysPerUser hold the name of the environment variable which
// sets the limit for number of API keys a single user can create. If a user
// reaches that limit they can always delete some API keys in order to make
// space for new ones.
envMaxNumAPIKeysPerUser = "ACCOUNTS_MAX_NUM_API_KEYS_PER_USER" // #nosec
)
type (
// ServiceConfig represents all configuration values we expect to receive
// via environment variables or config files.
ServiceConfig struct {
DBCreds database.DBCredentials
PortalName string
PortalAddressAccounts string
Promoter string
ServerLockID string
StripeKey string
JWKSFile string
JWTTTL int
EmailURI string
EmailFrom string
MaxAPIKeys int
}
)
// loadDBCredentials creates a new DB connection based on credentials found in
// the environment variables.
func loadDBCredentials() (database.DBCredentials, error) {
var cds database.DBCredentials
var ok bool
if cds.User, ok = os.LookupEnv(envDBUser); !ok {
return database.DBCredentials{}, errors.New("missing env var " + envDBUser)
}
if cds.Password, ok = os.LookupEnv(envDBPass); !ok {
return database.DBCredentials{}, errors.New("missing env var " + envDBPass)
}
if cds.Host, ok = os.LookupEnv(envDBHost); !ok {
return database.DBCredentials{}, errors.New("missing env var " + envDBHost)
}
if cds.Port, ok = os.LookupEnv(envDBPort); !ok {
return database.DBCredentials{}, errors.New("missing env var " + envDBPort)
}
return cds, nil
}
// logLevel returns the desires log level.
func logLevel() logrus.Level {
lvl, err := logrus.ParseLevel(os.Getenv(envLogLevel))
if err == nil {
return lvl
}
if build.DEBUG {
return logrus.TraceLevel
}
if build.Release == "testing" || build.Release == "dev" {
return logrus.DebugLevel
}
return logrus.InfoLevel
}
// parseConfiguration is responsible for reading and validating all environment
// variables we support - both required and optional ones. It also defers to the
// default values when certain config values are not provided. If in the future
// we add a config file for this service, it should be handled here as well.
func parseConfiguration(logger *logrus.Logger) (ServiceConfig, error) {
config := ServiceConfig{}
var err error
config.DBCreds, err = loadDBCredentials()
if err != nil {
log.Fatal(errors.AddContext(err, "failed to fetch DB credentials"))
}
// portal tells us which Skynet portal to use for downloading skylinks.
portal := os.Getenv(envPortal)
if portal == "" {
return ServiceConfig{}, errors.New("missing env var " + envPortal)
}
config.PortalName = "https://" + portal
config.PortalAddressAccounts = "https://account." + portal
config.Promoter = api.PromoterStripe
if val, ok := os.LookupEnv(envPromoter); ok {
switch strings.ToLower(val) {
case api.PromoterStripe:
// nothing to do
case api.PromoterPromoter:
config.Promoter = api.PromoterPromoter
default:
return ServiceConfig{}, errors.New("invalid value for env var " + envPromoter)
}
}
if config.Promoter == api.PromoterStripe {
if sk := os.Getenv(envStripeAPIKey); sk != "" {
config.StripeKey = sk
}
}
config.ServerLockID = os.Getenv(envServerDomain)
if config.ServerLockID == "" {
config.ServerLockID = config.PortalName
logger.Warningf(`Environment variable %s is missing! This server's identity`+
` is set to the default '%s' value. That is OK only if this server is running on its own`+
` and it's not sharing its DB with other nodes.`, envServerDomain, config.ServerLockID)
}
if jwks := os.Getenv(envAccountsJWKSFile); jwks != "" {
config.JWKSFile = jwks
} else {
config.JWKSFile = jwt.AccountsJWKSFile
}
// Parse the optional env var that controls the TTL of the JWTs we generate.
if jwtTTLStr := os.Getenv(envJWTTTL); jwtTTLStr != "" {
jwtTTL, err := strconv.Atoi(jwtTTLStr)
if err != nil {
return ServiceConfig{}, fmt.Errorf("failed to parse env var %s: %s", envJWTTTL, err)
}
if jwtTTL == 0 {
return ServiceConfig{}, fmt.Errorf("the %s env var is set to zero, which is an invalid value (must be positive or unset)", envJWTTTL)
}
config.JWTTTL = jwtTTL
} else {
// The environment doesn't specify a value, use the default.
config.JWTTTL = jwt.TTL
}
// Fetch configuration data for sending emails.
config.EmailURI = os.Getenv(envEmailURI)
{
if config.EmailURI == "" {
return ServiceConfig{}, email.ErrInvalidEmailConfiguration
}
// Validate the given URI.
uri, err := url.Parse(config.EmailURI)
if err != nil || uri.Host == "" || uri.User == nil {
return ServiceConfig{}, email.ErrInvalidEmailConfiguration
}
// Set the FROM address to outgoing emails. This can be overridden by
// the ACCOUNTS_EMAIL_FROM optional environment variable.
if uri.User != nil {
config.EmailFrom = uri.User.Username()
}
if emailFrom := os.Getenv(envEmailFrom); emailFrom != "" {
config.EmailFrom = emailFrom
}
// No custom value set, use the default.
if config.EmailFrom == "" {
config.EmailFrom = email.From
}
}
// Fetch the configuration for maximum number of API keys allowed per user.
if maxAPIKeysStr, exists := os.LookupEnv(envMaxNumAPIKeysPerUser); exists {
maxAPIKeys, err := strconv.Atoi(maxAPIKeysStr)
if err != nil {
log.Printf("Warning: Failed to parse %s env var. Error: %s", envMaxNumAPIKeysPerUser, err.Error())
}
if maxAPIKeys > 0 {
config.MaxAPIKeys = maxAPIKeys
} else {
log.Printf("Warning: Invalid value of %s. The invalid value is ignored and the default value of %d is used.", envMaxNumAPIKeysPerUser, database.MaxNumAPIKeysPerUser)
config.MaxAPIKeys = database.MaxNumAPIKeysPerUser
}
} else {
// The environment doesn't specify a value, use the default.
config.MaxAPIKeys = database.MaxNumAPIKeysPerUser
}
return config, nil
}
func main() {
// Initialise the global context and logger. These will be used throughout
// the service. Once the context is closed, all background threads will
// wind themselves down.
ctx := context.Background()
logger := logrus.New()
logger.SetLevel(logLevel())
// Load the environment variables from the .env file.
_ = godotenv.Load()
config, err := parseConfiguration(logger)
if err != nil {
log.Fatal(err)
}
database.PortalName = config.PortalName
jwt.PortalName = config.PortalName
email.PortalAddressAccounts = config.PortalAddressAccounts
api.DashboardURL = config.PortalAddressAccounts
email.ServerLockID = config.ServerLockID
stripe.Key = config.StripeKey
jwt.AccountsJWKSFile = config.JWKSFile
jwt.TTL = config.JWTTTL
email.From = config.EmailFrom
database.MaxNumAPIKeysPerUser = config.MaxAPIKeys
// Set up key components:
// Load the JWKS that we'll use to sign and validate JWTs.
err = jwt.LoadAccountsKeySet(logger)
if err != nil {
log.Fatal(errors.AddContext(err, fmt.Sprintf("failed to load JWKS file from %s", jwt.AccountsJWKSFile)))
}
// Connect to the database.
db, err := database.New(ctx, config.DBCreds, logger)
if err != nil {
log.Fatal(errors.AddContext(err, "failed to connect to the DB"))
}
mailer := email.NewMailer(db)
// Start the mail sender background thread.
sender, err := email.NewSender(ctx, db, logger, &skymodules.SkynetDependencies{}, config.EmailURI)
if err != nil {
log.Fatal(errors.AddContext(err, "failed to create an email sender"))
}
sender.Start()
// The meta fetcher will fetch metadata for all skylinks. This is needed, so
// we can determine their size.
mf := metafetcher.New(ctx, db, logger)
// Start the HTTP server.
server, err := api.New(db, mf, logger, mailer, config.Promoter)
if err != nil {
log.Fatal(errors.AddContext(err, "failed to build the API"))
}
log.Printf("Starting Accounts.\nGitRevision: %v (built %v)\n", build.GitRevision, build.BuildTime)
logger.Fatal(server.ListenAndServe(3000))
}