forked from decred/dcrstakepool
-
Notifications
You must be signed in to change notification settings - Fork 0
/
user.go
323 lines (270 loc) · 9.92 KB
/
user.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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package models
import (
"database/sql"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-gorp/gorp"
_ "github.com/go-sql-driver/mysql"
"golang.org/x/crypto/bcrypt"
)
type EmailChange struct {
Id int64 `db:"EmailChangeID"`
UserId int64
NewEmail string
Token string
Created int64
Expires int64
}
type PasswordReset struct {
Id int64 `db:"PasswordResetID"`
UserId int64
Token string
Created int64
Expires int64
}
type User struct {
Id int64 `db:"UserId"`
Email string
Username string
Password []byte
MultiSigAddress string
MultiSigScript string
PoolPubKeyAddr string
UserPubKeyAddr string
UserFeeAddr string
HeightRegistered int64
EmailVerified int64
EmailToken string
APIToken string
VoteBits int64
VoteBitsVersion int64
}
func (user *User) HashPassword(password string) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Criticalf("Couldn't hash password: %v", err)
panic(err)
}
user.Password = hash
}
func GetUserByEmail(dbMap *gorp.DbMap, email string) (user *User) {
err := dbMap.SelectOne(&user, "SELECT * FROM Users where Email = ?", email)
if err != nil {
log.Warnf("Can't get user by email: %v", err)
}
return
}
func GetUserById(dbMap *gorp.DbMap, id int64) (user *User, err error) {
err = dbMap.SelectOne(&user, "SELECT * FROM Users WHERE UserId = ?", id)
if err != nil {
return nil, err
}
return user, nil
}
// GetUserCount gives a count of all users
func GetUserCount(dbMap *gorp.DbMap) int64 {
userCount, err := dbMap.SelectInt("SELECT COUNT(*) FROM Users")
if err != nil {
return int64(0)
}
return userCount
}
// GetUserMax gives the last userid
func GetUserMax(dbMap *gorp.DbMap) int64 {
maxUserID, err := dbMap.SelectInt("SELECT MAX(Userid) FROM Users")
if err != nil {
return int64(0)
}
return maxUserID
}
// GetUserCountActive gives a count of all users who have submitted an address
func GetUserCountActive(dbMap *gorp.DbMap) int64 {
userCountActive, err := dbMap.SelectInt("SELECT COUNT(*) FROM Users " +
"WHERE MultiSigAddress <> ''")
if err != nil {
return int64(0)
}
return userCountActive
}
func InsertEmailChange(dbMap *gorp.DbMap, emailChange *EmailChange) error {
return dbMap.Insert(emailChange)
}
// InsertUser inserts a user into the DB
func InsertUser(dbMap *gorp.DbMap, user *User) error {
return dbMap.Insert(user)
}
func InsertPasswordReset(dbMap *gorp.DbMap, passwordReset *PasswordReset) error {
return dbMap.Insert(passwordReset)
}
// SetUserAPIToken generates and saves a unique API Token for a user.
func SetUserAPIToken(dbMap *gorp.DbMap, APISecret string, baseURL string,
id int64) error {
var user *User
token := jwt.New(jwt.SigningMethodHS256)
claims := make(jwt.MapClaims)
claims["iat"] = time.Now().Unix()
claims["iss"] = baseURL
claims["loggedInAs"] = id
token.Claims = claims
tokenString, err := token.SignedString([]byte(APISecret))
if err != nil {
return err
}
err = dbMap.SelectOne(&user, "SELECT * FROM Users WHERE UserId = ?", id)
user.APIToken = tokenString
if err != nil {
return err
}
_, err = dbMap.Update(user)
return err
}
// UpdateUserByID updates a user, specified by id, in the DB with a new
// multiSigAddr, multiSigScript, multiSigScript, pool pubkey address,
// user pub key address, and fee address. Unchanged are the user's ID, email,
// username and password.
func UpdateUserByID(dbMap *gorp.DbMap, id int64, multiSigAddr string,
multiSigScript string, poolPubKeyAddr string, userPubKeyAddr string,
userFeeAddr string, height int64) (user *User) {
err := dbMap.SelectOne(&user, "SELECT * FROM Users WHERE UserId = ?", id)
if err != nil {
log.Warnf("Can't get user by id: %v", err)
}
user.MultiSigAddress = multiSigAddr
user.MultiSigScript = multiSigScript
user.PoolPubKeyAddr = poolPubKeyAddr
user.UserPubKeyAddr = userPubKeyAddr
user.UserFeeAddr = userFeeAddr
user.HeightRegistered = height
_, err = dbMap.Update(user)
if err != nil {
log.Warnf("Couldn't update user: %v", err)
}
// return updated User
return
}
func GetAllCurrentMultiSigScripts(dbMap *gorp.DbMap) ([]User, error) {
var multiSigs []User
_, err := dbMap.Select(&multiSigs, "SELECT MultiSigScript, HeightRegistered FROM Users WHERE MultiSigAddress <> ''")
if err != nil {
return nil, err
}
return multiSigs, nil
}
func GetDbMap(APISecret, baseURL, user, password, hostname, port, database string) *gorp.DbMap {
// connect to db using standard Go database/sql API
// use whatever database/sql driver you wish
//TODO: Get user, password and database from config.
db, err := sql.Open("mysql", fmt.Sprint(user, ":", password, "@(", hostname, ":", port, ")/", database, "?charset=utf8mb4"))
if err != nil {
log.Critical("sql.Open failed: ", err)
return nil
}
// sql.Open just validates its arguments without creating a connection
// Verify that the data source name is valid with Ping:
if err = db.Ping(); err != nil {
log.Critical("Unable to establish connection to database: ", err)
return nil
}
// construct a gorp DbMap
dbMap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8MB4"}}
// add a table, setting the table name and specifying that
// the Id property is an auto incrementing primary key
dbMap.AddTableWithName(EmailChange{}, "EmailChange").SetKeys(true, "Id")
dbMap.AddTableWithName(PasswordReset{}, "PasswordReset").SetKeys(true, "Id")
dbMap.AddTableWithName(User{}, "Users").SetKeys(true, "Id")
// create the table. in a production system you'd generally
// use a migration tool, or create the tables via scripts
err = dbMap.CreateTablesIfNotExists()
if err != nil {
log.Critical("Create tables failed: ", err)
// There is no point proceeding, so return. TODO: signal to caller the
// error, or possibly close the db, or panic.
return dbMap
}
// The ORM, Gorp, doesn't support migrations so we just add new columns
// that weren't present in the original schema so admins can upgrade
// without manual intervention.
// stakepool v0.0.1 -> v0.0.2
// add HeightRegistered so dcrwallet doesn't scan from the genesis block
// for transactions that won't exist.
// The stake pool code was released to stake pool operators on Friday,
// April 1st 2016. The last mainnet block on Mar 31st of 15346 is used
// as a safe default to ensure no tickets are missed. This could be
// adjusted upwards since most pools were on testnet for a long time.
addColumn(dbMap, database, "Users", "HeightRegistered", "bigint(20) NULL",
"UserFeeAddr",
"UPDATE Users SET HeightRegistered = 15346 WHERE MultiSigAddress <> ''")
// stakepool v0.0.2 -> v0.0.3
// bug fix for previous -- users who hadn't submitted a script won't be
// able to login because Gorp can't handle NULL values
_, err = dbMap.Exec("UPDATE Users SET HeightRegistered = 0 WHERE HeightRegistered IS NULL")
if err != nil {
log.Error("Setting HeightRegistered to 0 failed ", err)
// Do not return since db is opened and other statements may work
}
// add EmailVerified, EmailToken so new users' email addresses can be
// verified. We consider users who already registered to be grandfathered
// in and use 2 to reflect that. 1 is verified, 0 is unverified.
addColumn(dbMap, database, "Users", "EmailVerified", "bigint(20) NULL",
"HeightRegistered",
"UPDATE Users SET EmailVerified = 2")
// Set an empty token for grandfathered accounts
addColumn(dbMap, database, "Users", "EmailToken", "varchar(255) NULL",
"EmailVerified",
"UPDATE Users SET EmailToken = ''")
// stakepool v0.0.4 -> v1.0.0
// add APIToken column for storing a token that users may use to submit a
// public key address and retrieve ticket purchasing information via the API
addColumn(dbMap, database, "Users", "APIToken", "varchar(255) NULL",
"EmailToken", "UPDATE Users SET APIToken = ''")
// Set an API token for all users who have verified their email address
// and do not have an API Token already set.
var users []User
_, err = dbMap.Select(&users, "SELECT * FROM Users WHERE APIToken = '' AND EmailVerified > 0")
if err != nil {
log.Critical("Select verified users failed: ", err)
// With out a Valid []Users, we cannot proceed
return dbMap
}
for _, u := range users {
err := SetUserAPIToken(dbMap, APISecret, baseURL, u.Id)
if err != nil {
log.Errorf("Unable to set API Token for UserId %v: %v", u.Id, err)
}
}
// stakepool v1.0.0 -> v1.1.0
// add VoteBits column for storing the user's voting preferences. Set the
// default to 1 which means the previous block was valid
addColumn(dbMap, database, "Users", "VoteBits", "bigint(20) NULL", "APIToken", "UPDATE Users SET VoteBits = 1")
// add VoteBitsVersion column for storing the vote version that the VoteBits
// are set for. The default is 3 since that is the current version on mainnet
// and it will be upgraded when talking to stakepoold
addColumn(dbMap, database, "Users", "VoteBitsVersion", "bigint(20) NULL", "VoteBits", "UPDATE Users SET VoteBitsVersion = 3")
return dbMap
}
// addColumn checks if a column exists and adds it if it doesn't
func addColumn(dbMap *gorp.DbMap, db string, table string, columnToAdd string,
dataSpec string, colAfter string, defaultQry string) {
s, err := dbMap.SelectStr("SELECT column_name FROM " +
"information_schema.columns WHERE table_schema = '" + db +
"' AND table_name = '" + table + "' AND column_name = '" +
columnToAdd + "'")
checkErr(err, "checking whether column"+columnToAdd+" exists failed")
if s == "" {
// TODO would be nice to use parameter binding here but gorp seems to
// only provide that for select queries
_, err = dbMap.Exec("ALTER TABLE `" + table + "` ADD COLUMN `" +
columnToAdd + "` " + dataSpec + " AFTER `" + colAfter + "`")
checkErr(err, "adding new column "+columnToAdd+" failed")
if defaultQry != "" {
_, err = dbMap.Exec(defaultQry)
checkErr(err, defaultQry+" failed")
}
}
}
func checkErr(err error, msg string) {
if err != nil {
log.Critical(msg, err)
}
}