A URL shortener service that uses ATProto to allow self hosting and ensuring the user owns their data
at main 3.4 kB view raw
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}