A URL shortener service that uses ATProto to allow self hosting and ensuring the user owns their data
1package database
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "fmt"
8 "log/slog"
9
10 "github.com/bluesky-social/indigo/atproto/auth/oauth"
11 "github.com/bluesky-social/indigo/atproto/syntax"
12)
13
14func createOauthSessionsTable(db *sql.DB) error {
15 createOauthSessionsTableSQL := `CREATE TABLE IF NOT EXISTS oauthsessions (
16 "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
17 "accountDID" TEXT,
18 "sessionID" TEXT,
19 "hostURL" TEXT,
20 "authServerURL" TEXT,
21 "authServerTokenEndpoint" TEXT,
22 "scopes" TEXT,
23 "accessToken" TEXT,
24 "refreshToken" TEXT,
25 "dpopAuthServerNonce" TEXT,
26 "dpopHostNonce" TEXT,
27 "dpopPrivateKeyMultibase" TEXT,
28 UNIQUE(accountDID,sessionID)
29 );`
30
31 slog.Info("Create oauthsessions table...")
32 statement, err := db.Prepare(createOauthSessionsTableSQL)
33 if err != nil {
34 return fmt.Errorf("prepare DB statement to create oauthsessions table: %w", err)
35 }
36 _, err = statement.Exec()
37 if err != nil {
38 return fmt.Errorf("exec sql statement to create oauthsessions table: %w", err)
39 }
40 slog.Info("oauthsessions table created")
41
42 return nil
43}
44
45func (d *DB) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error {
46 scopes, err := json.Marshal(sess.Scopes)
47 if err != nil {
48 return fmt.Errorf("marshalling scopes: %w", err)
49 }
50
51 sql := `INSERT INTO oauthsessions (accountDID, sessionID, hostURL, authServerURL, authServerTokenEndpoint, scopes, accessToken, refreshToken, dpopAuthServerNonce, dpopHostNonce, dpopPrivateKeyMultibase) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(accountDID,sessionID) DO NOTHING;`
52 _, err = d.db.Exec(sql, sess.AccountDID.String(), sess.SessionID, sess.HostURL, sess.AuthServerURL, sess.AuthServerTokenEndpoint, string(scopes), sess.AccessToken, sess.RefreshToken, sess.DPoPAuthServerNonce, sess.DPoPHostNonce, sess.DPoPPrivateKeyMultibase)
53 if err != nil {
54 slog.Error("saving session", "error", err)
55 return fmt.Errorf("exec insert oauth session: %w", err)
56 }
57
58 return nil
59}
60
61func (d *DB) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) {
62 var session oauth.ClientSessionData
63 sql := "SELECT hostURL, authServerURL, authServerTokenEndpoint, scopes, accessToken, refreshToken, dpopAuthServerNonce, dpopHostNonce, dpopPrivateKeyMultibase FROM oauthsessions where accountDID = ? AND sessionID = ?;"
64 rows, err := d.db.Query(sql, did.String(), sessionID)
65 if err != nil {
66 return nil, fmt.Errorf("run query to get oauth session: %w", err)
67 }
68 defer rows.Close()
69
70 scopes := ""
71 for rows.Next() {
72 if err := rows.Scan(&session.HostURL, &session.AuthServerURL, &session.AuthServerTokenEndpoint, &scopes, &session.AccessToken, &session.RefreshToken, &session.DPoPAuthServerNonce, &session.DPoPHostNonce, &session.DPoPPrivateKeyMultibase); err != nil {
73 return nil, fmt.Errorf("scan row: %w", err)
74 }
75 session.AccountDID = did
76
77 var parsedScopes []string
78 err = json.Unmarshal([]byte(scopes), &parsedScopes)
79 if err != nil {
80 return nil, fmt.Errorf("parsing scopes: %w", err)
81 }
82
83 session.Scopes = parsedScopes
84
85 return &session, nil
86 }
87 return nil, fmt.Errorf("not found")
88}
89
90func (d *DB) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error {
91 sql := "DELETE FROM oauthsessions WHERE accountDID = ?;"
92 _, err := d.db.Exec(sql, did.String())
93 if err != nil {
94 return fmt.Errorf("exec delete oauth session: %w", err)
95 }
96 return nil
97}