-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathuser.go
225 lines (189 loc) · 6.39 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
package main
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"github.com/rdbell/nvote/schemas"
"github.com/labstack/echo/v4"
"github.com/rdbell/go-nostr"
"github.com/rdbell/go-nostr/nip06"
)
// userRoutes sets up auth/account-related routes
func userRoutes(e *echo.Echo) {
e.POST("/logout", isLoggedIn(logoutHandler))
e.GET("/login", isLoggedOut(loginHandler))
e.GET("/alt_login", isLoggedOut(altLoginHandler))
e.POST("/login", isLoggedOut(loginSubmitHandler))
e.GET("/settings", isLoggedIn(settingsHandler))
e.POST("/settings", isLoggedIn(settingsSubmitHandler))
e.GET("/verify", isLoggedIn(isNotVerified(verifyHandler)))
e.GET("/u/:pubkey", activityHandler)
}
// logoutHandler logs a user out and redirects to the home page
func logoutHandler(c echo.Context) error {
clearCookie(c, "user")
return c.Redirect(http.StatusFound, "/")
}
// verifyHandler serves the page for registering a user's pubkey with the nostr relay
func verifyHandler(c echo.Context) error {
pd := new(pageData).Init(c)
pd.Title = "Veriy Account"
return c.Render(http.StatusOK, "base:verify", pd)
}
// loginHandler serves the login page
func loginHandler(c echo.Context) error {
var page struct {
SuggestedSeed string
}
// Generate suggested seed
var err error
page.SuggestedSeed, err = nip06.GenerateSeedWords()
if err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
pd := new(pageData).Init(c)
pd.Page = page
pd.Title = "Login"
return c.Render(http.StatusOK, "base:login", pd)
}
// altLoginHandler serves the alternative login page
func altLoginHandler(c echo.Context) error {
pd := new(pageData).Init(c)
pd.Title = "Login"
return c.Render(http.StatusOK, "base:alt_login", pd)
}
// settingsHandler serves the settings page
func settingsHandler(c echo.Context) error {
pd := new(pageData).Init(c)
pd.Title = "Settings"
return c.Render(http.StatusOK, "base:settings", pd)
}
// loginSubmitHandler sets a user cookie in the browser
func loginSubmitHandler(c echo.Context) error {
// Read form data
login := &schemas.Login{}
if err := c.Bind(login); err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
privkey, err := login.GeneratePrivateKey()
if err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
// Derive private key and set auth cookie
pubkey, err := nostr.GetPublicKey(privkey)
if err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
user := schemas.LoggedOutUser()
user.PubKey = pubkey
user.PrivKey = privkey
userJSON, err := json.Marshal(user)
if err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
// Save cookie
setCookie(c, "user", string(userJSON), time.Time{})
return c.Redirect(http.StatusFound, "/settings")
}
// settingsSubmitHandler sets a user cookie in the browser
func settingsSubmitHandler(c echo.Context) error {
// Read form data
user := &schemas.User{}
if err := c.Bind(user); err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
// Ensure private/public keys are still valid
pubkey, err := nostr.GetPublicKey(user.PrivKey)
if err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
if user.PubKey != pubkey {
return serveError(c, http.StatusInternalServerError, errors.New("invalid user object - try logging out"))
}
// Query for existing metadata
metadata, _ := metadataForPubkey(user.PubKey)
// Upsert metadata if changed
if (metadata != nil && user.Name != metadata.Name) || (metadata == nil && user.Name != "") || (metadata != nil && user.About != metadata.About) || (metadata == nil && user.About != "") {
metadata := &schemas.Metadata{
PubKey: user.PubKey,
Name: user.Name,
About: user.About,
}
// Validate new metadata
metadata.PrepareForPublish()
if !metadata.IsValid() {
return serveError(c, http.StatusInternalServerError, errors.New("invalid metadata"))
}
// Serialize data
content, err := json.Marshal(metadata)
if err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
// Publish metadata update event
_, err = publishEvent(c, content, nostr.KindSetMetadata, nil)
if err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
}
// Save cookie
user.Name = "" // don't need to save user.name client-side
user.About = "" // don't need to save user.about client-side
userJSON, err := json.Marshal(user)
if err != nil {
return serveError(c, http.StatusInternalServerError, err)
}
setCookie(c, "user", string(userJSON), time.Time{})
return c.Redirect(http.StatusFound, "/")
}
// generatedUsername generates a random username based on a provided pubkey
func generatedUsername(pubkey string) string {
// Ensure length
if len(pubkey) < 15 {
return "user"
}
// Use random name if no name provided
// Convert pubkey string to integer from base16 hex
// use [0:15] to prevent value out of range
i, err := strconv.ParseUint(pubkey[0:15], 16, 64)
if err != nil {
return pubkey[0:8]
}
// New random source
random := rand.New(rand.NewSource(int64(i)))
w1 := bip39WordList[random.Intn(len(bip39WordList))]
w2 := bip39WordList[random.Intn(len(bip39WordList))]
randomNumber := random.Intn(9999)
return fmt.Sprintf("%v%v%d", strings.Title(w1), strings.Title(w2), randomNumber)
}
// metadataForPubkey queries the DB and returns a *schemas.Metadata for a given pubkey
func metadataForPubkey(pubkey string) (*schemas.Metadata, error) {
metadata := &schemas.Metadata{}
metadata.PubKey = pubkey
db.QueryRow(`SELECT SUM(score) FROM posts WHERE pubkey = ?`, pubkey).Scan(&metadata.UserScore)
err := db.QueryRow(`SELECT name, about, created_at FROM metadata WHERE pubkey = ?`, pubkey).Scan(&metadata.Name, &metadata.About, &metadata.CreatedAt)
if err != nil || metadata.Name == "" {
metadata.Name = generatedUsername(pubkey)
}
return metadata, nil
}
// upsertMetadata upserts a user's metadata into the DB
func upsertMetadata(metadata *schemas.Metadata) error {
// Sanitize before upsert
metadata.Sanitize()
// Validate
if !metadata.IsValid() {
return errors.New("invalid metadata")
}
// Add to DB
_, err := db.Exec(`INSERT INTO metadata(pubkey, name, about, created_at) VALUES(?,?,?,?) ON CONFLICT (pubkey) DO UPDATE SET name=?, about=?, created_at=?`,
metadata.PubKey, metadata.Name, metadata.About, metadata.CreatedAt, metadata.Name, metadata.About, metadata.CreatedAt)
if err != nil {
return err
}
return nil
}