1package state
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "log"
8 "net/http"
9 "strings"
10 "time"
11
12 comatproto "github.com/bluesky-social/indigo/api/atproto"
13 lexutil "github.com/bluesky-social/indigo/lex/util"
14 "github.com/gliderlabs/ssh"
15 "github.com/google/uuid"
16 "github.com/sotangled/tangled/api/tangled"
17 "github.com/sotangled/tangled/appview/db"
18 "github.com/sotangled/tangled/appview/email"
19 "github.com/sotangled/tangled/appview/pages"
20)
21
22func (s *State) Settings(w http.ResponseWriter, r *http.Request) {
23 user := s.auth.GetUser(r)
24 pubKeys, err := db.GetPublicKeys(s.db, user.Did)
25 if err != nil {
26 log.Println(err)
27 }
28
29 emails, err := db.GetAllEmails(s.db, user.Did)
30 if err != nil {
31 log.Println(err)
32 }
33
34 s.pages.Settings(w, pages.SettingsParams{
35 LoggedInUser: user,
36 PubKeys: pubKeys,
37 Emails: emails,
38 })
39}
40
41func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) {
42 switch r.Method {
43 case http.MethodGet:
44 s.pages.Notice(w, "settings-emails", "Unimplemented.")
45 log.Println("unimplemented")
46 return
47 case http.MethodPut:
48 did := s.auth.GetDid(r)
49 emAddr := r.FormValue("email")
50 emAddr = strings.TrimSpace(emAddr)
51
52 if !email.IsValidEmail(emAddr) {
53 s.pages.Notice(w, "settings-emails-error", "Invalid email address.")
54 return
55 }
56
57 // check if email already exists in database
58 existingEmail, err := db.GetEmail(s.db, did, emAddr)
59 if err != nil && !errors.Is(err, sql.ErrNoRows) {
60 log.Printf("checking for existing email: %s", err)
61 s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
62 return
63 }
64
65 if err == nil {
66 if existingEmail.Verified {
67 s.pages.Notice(w, "settings-emails-error", "This email is already verified.")
68 return
69 }
70
71 s.pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.")
72 return
73 }
74
75 code := uuid.New().String()
76
77 // Begin transaction
78 tx, err := s.db.Begin()
79 if err != nil {
80 log.Printf("failed to start transaction: %s", err)
81 s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
82 return
83 }
84 defer tx.Rollback()
85
86 if err := db.AddEmail(tx, db.Email{
87 Did: did,
88 Address: emAddr,
89 Verified: false,
90 VerificationCode: code,
91 }); err != nil {
92 log.Printf("adding email: %s", err)
93 s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
94 return
95 }
96
97 err = email.SendEmail(email.Email{
98 APIKey: s.config.ResendApiKey,
99
100 From: "noreply@notifs.tangled.sh",
101 To: emAddr,
102 Subject: "Verify your Tangled email",
103 Text: `Click the link below (or copy and paste it into your browser) to verify your email address.
104` + s.verifyUrl(did, emAddr, code),
105 Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p>
106<p><a href="` + s.verifyUrl(did, emAddr, code) + `">` + s.verifyUrl(did, emAddr, code) + `</a></p>`,
107 })
108
109 if err != nil {
110 log.Printf("sending email: %s", err)
111 s.pages.Notice(w, "settings-emails-error", "Unable to send verification email at this moment, try again later.")
112 return
113 }
114
115 // Commit transaction
116 if err := tx.Commit(); err != nil {
117 log.Printf("failed to commit transaction: %s", err)
118 s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.")
119 return
120 }
121
122 s.pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.")
123 return
124 case http.MethodDelete:
125 did := s.auth.GetDid(r)
126 emailAddr := r.FormValue("email")
127 emailAddr = strings.TrimSpace(emailAddr)
128
129 // Begin transaction
130 tx, err := s.db.Begin()
131 if err != nil {
132 log.Printf("failed to start transaction: %s", err)
133 s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
134 return
135 }
136 defer tx.Rollback()
137
138 if err := db.DeleteEmail(tx, did, emailAddr); err != nil {
139 log.Printf("deleting email: %s", err)
140 s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
141 return
142 }
143
144 // Commit transaction
145 if err := tx.Commit(); err != nil {
146 log.Printf("failed to commit transaction: %s", err)
147 s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.")
148 return
149 }
150
151 s.pages.HxLocation(w, "/settings")
152 return
153 }
154}
155
156func (s *State) verifyUrl(did string, email string, code string) string {
157 var appUrl string
158 if s.config.Dev {
159 appUrl = "http://" + s.config.ListenAddr
160 } else {
161 appUrl = "https://tangled.sh"
162 }
163
164 return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, did, email, code)
165}
166
167func (s *State) SettingsEmailsVerify(w http.ResponseWriter, r *http.Request) {
168 q := r.URL.Query()
169
170 // Get the parameters directly from the query
171 emailAddr := q.Get("email")
172 did := q.Get("did")
173 code := q.Get("code")
174
175 valid, err := db.CheckValidVerificationCode(s.db, did, emailAddr, code)
176 if err != nil {
177 log.Printf("checking email verification: %s", err)
178 s.pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.")
179 return
180 }
181
182 if !valid {
183 s.pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.")
184 return
185 }
186
187 // Mark email as verified in the database
188 if err := db.MarkEmailVerified(s.db, did, emailAddr); err != nil {
189 log.Printf("marking email as verified: %s", err)
190 s.pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.")
191 return
192 }
193
194 http.Redirect(w, r, "/settings", http.StatusSeeOther)
195}
196
197func (s *State) SettingsEmailsPrimary(w http.ResponseWriter, r *http.Request) {
198 did := s.auth.GetDid(r)
199 emailAddr := r.FormValue("email")
200 emailAddr = strings.TrimSpace(emailAddr)
201
202 if emailAddr == "" {
203 s.pages.Notice(w, "settings-emails-error", "Email address cannot be empty.")
204 return
205 }
206
207 if err := db.MakeEmailPrimary(s.db, did, emailAddr); err != nil {
208 log.Printf("setting primary email: %s", err)
209 s.pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.")
210 return
211 }
212
213 s.pages.HxLocation(w, "/settings")
214}
215
216func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) {
217 switch r.Method {
218 case http.MethodGet:
219 s.pages.Notice(w, "settings-keys", "Unimplemented.")
220 log.Println("unimplemented")
221 return
222 case http.MethodPut:
223 did := s.auth.GetDid(r)
224 key := r.FormValue("key")
225 key = strings.TrimSpace(key)
226 name := r.FormValue("name")
227 client, _ := s.auth.AuthorizedClient(r)
228
229 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
230 if err != nil {
231 log.Printf("parsing public key: %s", err)
232 s.pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.")
233 return
234 }
235
236 rkey := s.TID()
237
238 tx, err := s.db.Begin()
239 if err != nil {
240 log.Printf("failed to start tx; adding public key: %s", err)
241 s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
242 return
243 }
244 defer tx.Rollback()
245
246 if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil {
247 log.Printf("adding public key: %s", err)
248 s.pages.Notice(w, "settings-keys", "Failed to add public key.")
249 return
250 }
251
252 // store in pds too
253 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
254 Collection: tangled.PublicKeyNSID,
255 Repo: did,
256 Rkey: rkey,
257 Record: &lexutil.LexiconTypeDecoder{
258 Val: &tangled.PublicKey{
259 Created: time.Now().Format(time.RFC3339),
260 Key: key,
261 Name: name,
262 }},
263 })
264 // invalid record
265 if err != nil {
266 log.Printf("failed to create record: %s", err)
267 s.pages.Notice(w, "settings-keys", "Failed to create record.")
268 return
269 }
270
271 log.Println("created atproto record: ", resp.Uri)
272
273 err = tx.Commit()
274 if err != nil {
275 log.Printf("failed to commit tx; adding public key: %s", err)
276 s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.")
277 return
278 }
279
280 s.pages.HxLocation(w, "/settings")
281 return
282
283 case http.MethodDelete:
284 did := s.auth.GetDid(r)
285 q := r.URL.Query()
286
287 name := q.Get("name")
288 rkey := q.Get("rkey")
289 key := q.Get("key")
290
291 log.Println(name)
292 log.Println(rkey)
293 log.Println(key)
294
295 client, _ := s.auth.AuthorizedClient(r)
296
297 if err := db.RemovePublicKey(s.db, did, name, key); err != nil {
298 log.Printf("removing public key: %s", err)
299 s.pages.Notice(w, "settings-keys", "Failed to remove public key.")
300 return
301 }
302
303 if rkey != "" {
304 // remove from pds too
305 _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
306 Collection: tangled.PublicKeyNSID,
307 Repo: did,
308 Rkey: rkey,
309 })
310
311 // invalid record
312 if err != nil {
313 log.Printf("failed to delete record from PDS: %s", err)
314 s.pages.Notice(w, "settings-keys", "Failed to remove key from PDS.")
315 return
316 }
317 }
318 log.Println("deleted successfully")
319
320 s.pages.HxLocation(w, "/settings")
321 return
322 }
323}