Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

backend improvements

+786 -118
+8 -10
.env.example
··· 1 - # Environment Configuration 1 + # Margin Server Configuration 2 2 3 3 # Server 4 4 PORT=8080 5 5 BASE_URL=https://example.com 6 6 7 - # Database 7 + # Database (SQLite file path or PostgreSQL connection string) 8 8 DATABASE_URL=margin.db 9 9 10 10 # Static Files (path to built frontend) 11 11 STATIC_DIR=../web/dist 12 12 13 - # AT Protocol OAuth 14 - OAUTH_CLIENT_ID=https://example.com/client-metadata.json 15 - OAUTH_CALLBACK_URL=https://example.com/auth/callback 13 + # OAuth private key for signing requests (auto-generated if missing) 16 14 OAUTH_KEY_PATH=./oauth_private_key.pem 17 15 18 - # Production Example: 19 - # PORT=443 20 - # BASE_URL=https://margin.at 21 - # OAUTH_CLIENT_ID=https://margin.at/client-metadata.json 22 - # OAUTH_CALLBACK_URL=https://margin.at/auth/callback 16 + 17 + # Optional: Override default ATProto network URLs (you probably don't need these) 18 + # BSKY_PUBLIC_API=https://public.api.bsky.app 19 + # PLC_DIRECTORY_URL=https://plc.directory 20 + # BLOCK_RELAY_URL=wss://jetstream2.us-east.bsky.network/subscribe
+9 -6
backend/cmd/server/main.go
··· 92 92 r.Put("/api/bookmarks", annotationSvc.UpdateBookmark) 93 93 r.Delete("/api/bookmarks", annotationSvc.DeleteBookmark) 94 94 95 - r.Get("/auth/login", oauthHandler.HandleLogin) 96 - r.Post("/auth/start", oauthHandler.HandleStart) 97 - r.Post("/auth/signup", oauthHandler.HandleSignup) 98 - r.Get("/auth/callback", oauthHandler.HandleCallback) 99 - r.Post("/auth/logout", oauthHandler.HandleLogout) 100 - r.Get("/auth/session", oauthHandler.HandleSession) 95 + r.Route("/auth", func(r chi.Router) { 96 + r.Use(middleware.Throttle(10)) 97 + r.Get("/login", oauthHandler.HandleLogin) 98 + r.Post("/start", oauthHandler.HandleStart) 99 + r.Post("/signup", oauthHandler.HandleSignup) 100 + r.Get("/callback", oauthHandler.HandleCallback) 101 + r.Post("/logout", oauthHandler.HandleLogout) 102 + r.Get("/session", oauthHandler.HandleSession) 103 + }) 101 104 r.Get("/client-metadata.json", oauthHandler.HandleClientMetadata) 102 105 r.Get("/jwks.json", oauthHandler.HandleJWKS) 103 106
+18
backend/internal/api/annotations.go
··· 391 391 return 392 392 } 393 393 394 + if req.SubjectURI == "" || req.SubjectCID == "" { 395 + http.Error(w, "subjectUri and subjectCid are required", http.StatusBadRequest) 396 + return 397 + } 398 + 394 399 existingLike, _ := s.db.GetLikeByUserAndSubject(session.DID, req.SubjectURI) 395 400 if existingLike != nil { 396 401 w.Header().Set("Content-Type", "application/json") ··· 494 499 var req CreateReplyRequest 495 500 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 496 501 http.Error(w, "Invalid request body", http.StatusBadRequest) 502 + return 503 + } 504 + 505 + if req.ParentURI == "" || req.ParentCID == "" { 506 + http.Error(w, "parentUri and parentCid are required", http.StatusBadRequest) 507 + return 508 + } 509 + if req.RootURI == "" || req.RootCID == "" { 510 + http.Error(w, "rootUri and rootCid are required", http.StatusBadRequest) 511 + return 512 + } 513 + if req.Text == "" { 514 + http.Error(w, "text is required", http.StatusBadRequest) 497 515 return 498 516 } 499 517
+58
backend/internal/api/errors.go
··· 1 + package api 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + ) 7 + 8 + type APIError struct { 9 + Error string `json:"error"` 10 + Code string `json:"code,omitempty"` 11 + Details string `json:"details,omitempty"` 12 + } 13 + 14 + func WriteJSONError(w http.ResponseWriter, statusCode int, message string) { 15 + w.Header().Set("Content-Type", "application/json") 16 + w.WriteHeader(statusCode) 17 + json.NewEncoder(w).Encode(APIError{Error: message}) 18 + } 19 + 20 + func WriteJSONErrorWithCode(w http.ResponseWriter, statusCode int, message, code string) { 21 + w.Header().Set("Content-Type", "application/json") 22 + w.WriteHeader(statusCode) 23 + json.NewEncoder(w).Encode(APIError{Error: message, Code: code}) 24 + } 25 + 26 + func WriteJSON(w http.ResponseWriter, statusCode int, data interface{}) { 27 + w.Header().Set("Content-Type", "application/json") 28 + w.WriteHeader(statusCode) 29 + json.NewEncoder(w).Encode(data) 30 + } 31 + 32 + func WriteSuccess(w http.ResponseWriter, data interface{}) { 33 + WriteJSON(w, http.StatusOK, data) 34 + } 35 + 36 + func WriteBadRequest(w http.ResponseWriter, message string) { 37 + WriteJSONError(w, http.StatusBadRequest, message) 38 + } 39 + 40 + func WriteUnauthorized(w http.ResponseWriter, message string) { 41 + WriteJSONError(w, http.StatusUnauthorized, message) 42 + } 43 + 44 + func WriteForbidden(w http.ResponseWriter, message string) { 45 + WriteJSONError(w, http.StatusForbidden, message) 46 + } 47 + 48 + func WriteNotFound(w http.ResponseWriter, message string) { 49 + WriteJSONError(w, http.StatusNotFound, message) 50 + } 51 + 52 + func WriteConflict(w http.ResponseWriter, message string) { 53 + WriteJSONError(w, http.StatusConflict, message) 54 + } 55 + 56 + func WriteInternalError(w http.ResponseWriter, message string) { 57 + WriteJSONError(w, http.StatusInternalServerError, message) 58 + }
+96 -12
backend/internal/api/handler.go
··· 168 168 if tag != "" { 169 169 if creator != "" { 170 170 if motivation == "" || motivation == "commenting" { 171 - annotations, _ = h.db.GetAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0) 171 + switch feedType { 172 + case "margin": 173 + annotations, _ = h.db.GetMarginAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0) 174 + case "semble": 175 + annotations, _ = h.db.GetSembleAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0) 176 + default: 177 + annotations, _ = h.db.GetAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0) 178 + } 172 179 } 173 180 if motivation == "" || motivation == "highlighting" { 174 - highlights, _ = h.db.GetHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0) 181 + switch feedType { 182 + case "margin": 183 + highlights, _ = h.db.GetMarginHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0) 184 + case "semble": 185 + highlights, _ = h.db.GetSembleHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0) 186 + default: 187 + highlights, _ = h.db.GetHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0) 188 + } 175 189 } 176 190 if motivation == "" || motivation == "bookmarking" { 177 - bookmarks, _ = h.db.GetBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0) 191 + switch feedType { 192 + case "margin": 193 + bookmarks, _ = h.db.GetMarginBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0) 194 + case "semble": 195 + bookmarks, _ = h.db.GetSembleBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0) 196 + default: 197 + bookmarks, _ = h.db.GetBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0) 198 + } 178 199 } 179 200 collectionItems = []db.CollectionItem{} 180 201 } else { 181 202 if motivation == "" || motivation == "commenting" { 182 - annotations, _ = h.db.GetAnnotationsByTag(tag, fetchLimit, 0) 203 + switch feedType { 204 + case "margin": 205 + annotations, _ = h.db.GetMarginAnnotationsByTag(tag, fetchLimit, 0) 206 + case "semble": 207 + annotations, _ = h.db.GetSembleAnnotationsByTag(tag, fetchLimit, 0) 208 + default: 209 + annotations, _ = h.db.GetAnnotationsByTag(tag, fetchLimit, 0) 210 + } 183 211 } 184 212 if motivation == "" || motivation == "highlighting" { 185 - highlights, _ = h.db.GetHighlightsByTag(tag, fetchLimit, 0) 213 + switch feedType { 214 + case "margin": 215 + highlights, _ = h.db.GetMarginHighlightsByTag(tag, fetchLimit, 0) 216 + case "semble": 217 + highlights, _ = h.db.GetSembleHighlightsByTag(tag, fetchLimit, 0) 218 + default: 219 + highlights, _ = h.db.GetHighlightsByTag(tag, fetchLimit, 0) 220 + } 186 221 } 187 222 if motivation == "" || motivation == "bookmarking" { 188 - bookmarks, _ = h.db.GetBookmarksByTag(tag, fetchLimit, 0) 223 + switch feedType { 224 + case "margin": 225 + bookmarks, _ = h.db.GetMarginBookmarksByTag(tag, fetchLimit, 0) 226 + case "semble": 227 + bookmarks, _ = h.db.GetSembleBookmarksByTag(tag, fetchLimit, 0) 228 + default: 229 + bookmarks, _ = h.db.GetBookmarksByTag(tag, fetchLimit, 0) 230 + } 189 231 } 190 232 collectionItems = []db.CollectionItem{} 191 233 } 192 234 } else if creator != "" { 193 235 if motivation == "" || motivation == "commenting" { 194 - annotations, _ = h.db.GetAnnotationsByAuthor(creator, fetchLimit, 0) 236 + switch feedType { 237 + case "margin": 238 + annotations, _ = h.db.GetMarginAnnotationsByAuthor(creator, fetchLimit, 0) 239 + case "semble": 240 + annotations, _ = h.db.GetSembleAnnotationsByAuthor(creator, fetchLimit, 0) 241 + default: 242 + annotations, _ = h.db.GetAnnotationsByAuthor(creator, fetchLimit, 0) 243 + } 195 244 } 196 245 if motivation == "" || motivation == "highlighting" { 197 - highlights, _ = h.db.GetHighlightsByAuthor(creator, fetchLimit, 0) 246 + switch feedType { 247 + case "margin": 248 + highlights, _ = h.db.GetMarginHighlightsByAuthor(creator, fetchLimit, 0) 249 + case "semble": 250 + highlights, _ = h.db.GetSembleHighlightsByAuthor(creator, fetchLimit, 0) 251 + default: 252 + highlights, _ = h.db.GetHighlightsByAuthor(creator, fetchLimit, 0) 253 + } 198 254 } 199 255 if motivation == "" || motivation == "bookmarking" { 200 - bookmarks, _ = h.db.GetBookmarksByAuthor(creator, fetchLimit, 0) 256 + switch feedType { 257 + case "margin": 258 + bookmarks, _ = h.db.GetMarginBookmarksByAuthor(creator, fetchLimit, 0) 259 + case "semble": 260 + bookmarks, _ = h.db.GetSembleBookmarksByAuthor(creator, fetchLimit, 0) 261 + default: 262 + bookmarks, _ = h.db.GetBookmarksByAuthor(creator, fetchLimit, 0) 263 + } 201 264 } 202 265 collectionItems = []db.CollectionItem{} 203 266 } else { 204 267 if motivation == "" || motivation == "commenting" { 205 - annotations, _ = h.db.GetRecentAnnotations(fetchLimit, 0) 268 + switch feedType { 269 + case "margin": 270 + annotations, _ = h.db.GetMarginAnnotations(fetchLimit, 0) 271 + case "semble": 272 + annotations, _ = h.db.GetSembleAnnotations(fetchLimit, 0) 273 + default: 274 + annotations, _ = h.db.GetRecentAnnotations(fetchLimit, 0) 275 + } 206 276 } 207 277 if motivation == "" || motivation == "highlighting" { 208 - highlights, _ = h.db.GetRecentHighlights(fetchLimit, 0) 278 + switch feedType { 279 + case "margin": 280 + highlights, _ = h.db.GetMarginHighlights(fetchLimit, 0) 281 + case "semble": 282 + highlights, _ = h.db.GetSembleHighlights(fetchLimit, 0) 283 + default: 284 + highlights, _ = h.db.GetRecentHighlights(fetchLimit, 0) 285 + } 209 286 } 210 287 if motivation == "" || motivation == "bookmarking" { 211 - bookmarks, _ = h.db.GetRecentBookmarks(fetchLimit, 0) 288 + switch feedType { 289 + case "margin": 290 + bookmarks, _ = h.db.GetMarginBookmarks(fetchLimit, 0) 291 + case "semble": 292 + bookmarks, _ = h.db.GetSembleBookmarks(fetchLimit, 0) 293 + default: 294 + bookmarks, _ = h.db.GetRecentBookmarks(fetchLimit, 0) 295 + } 212 296 } 213 297 if motivation == "" { 214 298 collectionItems, err = h.db.GetRecentCollectionItems(fetchLimit, 0)
+2 -1
backend/internal/api/hydration.go
··· 11 11 "sync" 12 12 "time" 13 13 14 + "margin.at/internal/config" 14 15 "margin.at/internal/constellation" 15 16 "margin.at/internal/db" 16 17 ) ··· 526 527 q.Add("actors", did) 527 528 } 528 529 529 - resp, err := http.Get("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfiles?" + q.Encode()) 530 + resp, err := http.Get(config.Get().BskyGetProfilesURL() + "?" + q.Encode()) 530 531 if err != nil { 531 532 log.Printf("Hydration fetch error: %v\n", err) 532 533 return nil, err
+47
backend/internal/config/config.go
··· 1 + package config 2 + 3 + import ( 4 + "os" 5 + "sync" 6 + ) 7 + 8 + type Config struct { 9 + BskyPublicAPI string 10 + PLCDirectory string 11 + BaseURL string 12 + } 13 + 14 + var ( 15 + instance *Config 16 + once sync.Once 17 + ) 18 + 19 + func Get() *Config { 20 + once.Do(func() { 21 + instance = &Config{ 22 + BskyPublicAPI: getEnvOrDefault("BSKY_PUBLIC_API", "https://public.api.bsky.app"), 23 + PLCDirectory: getEnvOrDefault("PLC_DIRECTORY_URL", "https://plc.directory"), 24 + BaseURL: os.Getenv("BASE_URL"), 25 + } 26 + }) 27 + return instance 28 + } 29 + 30 + func getEnvOrDefault(key, defaultValue string) string { 31 + if value := os.Getenv(key); value != "" { 32 + return value 33 + } 34 + return defaultValue 35 + } 36 + 37 + func (c *Config) BskyResolveHandleURL(handle string) string { 38 + return c.BskyPublicAPI + "/xrpc/com.atproto.identity.resolveHandle?handle=" + handle 39 + } 40 + 41 + func (c *Config) BskyGetProfilesURL() string { 42 + return c.BskyPublicAPI + "/xrpc/app.bsky.actor.getProfiles" 43 + } 44 + 45 + func (c *Config) PLCResolveURL(did string) string { 46 + return c.PLCDirectory + "/" + did 47 + }
+2 -2
backend/internal/db/db.go
··· 150 150 151 151 db, err := sql.Open(driver, dsn) 152 152 if err != nil { 153 - return nil, err 153 + return nil, fmt.Errorf("failed to open database connection: %w", err) 154 154 } 155 155 156 156 if driver == "sqlite3" { ··· 172 172 } 173 173 174 174 if err := db.Ping(); err != nil { 175 - return nil, err 175 + return nil, fmt.Errorf("failed to ping database: %w", err) 176 176 } 177 177 178 178 return &DB{DB: db, driver: driver}, nil
+132
backend/internal/db/queries_annotations.go
··· 67 67 return scanAnnotations(rows) 68 68 } 69 69 70 + func (db *DB) GetMarginAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) { 71 + rows, err := db.Query(db.Rebind(` 72 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 73 + FROM annotations 74 + WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%' 75 + ORDER BY created_at DESC 76 + LIMIT ? OFFSET ? 77 + `), authorDID, limit, offset) 78 + if err != nil { 79 + return nil, err 80 + } 81 + defer rows.Close() 82 + 83 + return scanAnnotations(rows) 84 + } 85 + 86 + func (db *DB) GetSembleAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) { 87 + rows, err := db.Query(db.Rebind(` 88 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 89 + FROM annotations 90 + WHERE author_did = ? AND uri LIKE '%network.cosmik%' 91 + ORDER BY created_at DESC 92 + LIMIT ? OFFSET ? 93 + `), authorDID, limit, offset) 94 + if err != nil { 95 + return nil, err 96 + } 97 + defer rows.Close() 98 + 99 + return scanAnnotations(rows) 100 + } 101 + 70 102 func (db *DB) GetAnnotationsByMotivation(motivation string, limit, offset int) ([]Annotation, error) { 71 103 rows, err := db.Query(db.Rebind(` 72 104 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid ··· 98 130 return scanAnnotations(rows) 99 131 } 100 132 133 + func (db *DB) GetMarginAnnotations(limit, offset int) ([]Annotation, error) { 134 + rows, err := db.Query(db.Rebind(` 135 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 136 + FROM annotations 137 + WHERE uri NOT LIKE '%network.cosmik%' 138 + ORDER BY created_at DESC 139 + LIMIT ? OFFSET ? 140 + `), limit, offset) 141 + if err != nil { 142 + return nil, err 143 + } 144 + defer rows.Close() 145 + 146 + return scanAnnotations(rows) 147 + } 148 + 149 + func (db *DB) GetSembleAnnotations(limit, offset int) ([]Annotation, error) { 150 + rows, err := db.Query(db.Rebind(` 151 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 152 + FROM annotations 153 + WHERE uri LIKE '%network.cosmik%' 154 + ORDER BY created_at DESC 155 + LIMIT ? OFFSET ? 156 + `), limit, offset) 157 + if err != nil { 158 + return nil, err 159 + } 160 + defer rows.Close() 161 + 162 + return scanAnnotations(rows) 163 + } 164 + 101 165 func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) { 102 166 pattern := "%\"" + tag + "\"%" 103 167 rows, err := db.Query(db.Rebind(` ··· 115 179 return scanAnnotations(rows) 116 180 } 117 181 182 + func (db *DB) GetMarginAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) { 183 + pattern := "%\"" + tag + "\"%" 184 + rows, err := db.Query(db.Rebind(` 185 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 186 + FROM annotations 187 + WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%' 188 + ORDER BY created_at DESC 189 + LIMIT ? OFFSET ? 190 + `), pattern, limit, offset) 191 + if err != nil { 192 + return nil, err 193 + } 194 + defer rows.Close() 195 + 196 + return scanAnnotations(rows) 197 + } 198 + 199 + func (db *DB) GetSembleAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) { 200 + pattern := "%\"" + tag + "\"%" 201 + rows, err := db.Query(db.Rebind(` 202 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 203 + FROM annotations 204 + WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%' 205 + ORDER BY created_at DESC 206 + LIMIT ? OFFSET ? 207 + `), pattern, limit, offset) 208 + if err != nil { 209 + return nil, err 210 + } 211 + defer rows.Close() 212 + 213 + return scanAnnotations(rows) 214 + } 215 + 118 216 func (db *DB) DeleteAnnotation(uri string) error { 119 217 _, err := db.Exec(db.Rebind(`DELETE FROM annotations WHERE uri = ?`), uri) 120 218 return err ··· 135 233 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 136 234 FROM annotations 137 235 WHERE author_did = ? AND tags_json LIKE ? 236 + ORDER BY created_at DESC 237 + LIMIT ? OFFSET ? 238 + `), authorDID, pattern, limit, offset) 239 + if err != nil { 240 + return nil, err 241 + } 242 + defer rows.Close() 243 + 244 + return scanAnnotations(rows) 245 + } 246 + 247 + func (db *DB) GetMarginAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) { 248 + pattern := "%\"" + tag + "\"%" 249 + rows, err := db.Query(db.Rebind(` 250 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 251 + FROM annotations 252 + WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%' 253 + ORDER BY created_at DESC 254 + LIMIT ? OFFSET ? 255 + `), authorDID, pattern, limit, offset) 256 + if err != nil { 257 + return nil, err 258 + } 259 + defer rows.Close() 260 + 261 + return scanAnnotations(rows) 262 + } 263 + 264 + func (db *DB) GetSembleAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) { 265 + pattern := "%\"" + tag + "\"%" 266 + rows, err := db.Query(db.Rebind(` 267 + SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid 268 + FROM annotations 269 + WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%' 138 270 ORDER BY created_at DESC 139 271 LIMIT ? OFFSET ? 140 272 `), authorDID, pattern, limit, offset)
+196
backend/internal/db/queries_bookmarks.go
··· 54 54 return bookmarks, nil 55 55 } 56 56 57 + func (db *DB) GetMarginBookmarks(limit, offset int) ([]Bookmark, error) { 58 + rows, err := db.Query(db.Rebind(` 59 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 60 + FROM bookmarks 61 + WHERE uri NOT LIKE '%network.cosmik%' 62 + ORDER BY created_at DESC 63 + LIMIT ? OFFSET ? 64 + `), limit, offset) 65 + if err != nil { 66 + return nil, err 67 + } 68 + defer rows.Close() 69 + 70 + var bookmarks []Bookmark 71 + for rows.Next() { 72 + var b Bookmark 73 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 74 + return nil, err 75 + } 76 + bookmarks = append(bookmarks, b) 77 + } 78 + return bookmarks, nil 79 + } 80 + 81 + func (db *DB) GetSembleBookmarks(limit, offset int) ([]Bookmark, error) { 82 + rows, err := db.Query(db.Rebind(` 83 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 84 + FROM bookmarks 85 + WHERE uri LIKE '%network.cosmik%' 86 + ORDER BY created_at DESC 87 + LIMIT ? OFFSET ? 88 + `), limit, offset) 89 + if err != nil { 90 + return nil, err 91 + } 92 + defer rows.Close() 93 + 94 + var bookmarks []Bookmark 95 + for rows.Next() { 96 + var b Bookmark 97 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 98 + return nil, err 99 + } 100 + bookmarks = append(bookmarks, b) 101 + } 102 + return bookmarks, nil 103 + } 104 + 57 105 func (db *DB) GetBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) { 58 106 pattern := "%\"" + tag + "\"%" 59 107 rows, err := db.Query(db.Rebind(` 60 108 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 61 109 FROM bookmarks 62 110 WHERE tags_json LIKE ? 111 + ORDER BY created_at DESC 112 + LIMIT ? OFFSET ? 113 + `), pattern, limit, offset) 114 + if err != nil { 115 + return nil, err 116 + } 117 + defer rows.Close() 118 + 119 + var bookmarks []Bookmark 120 + for rows.Next() { 121 + var b Bookmark 122 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 123 + return nil, err 124 + } 125 + bookmarks = append(bookmarks, b) 126 + } 127 + return bookmarks, nil 128 + } 129 + 130 + func (db *DB) GetMarginBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) { 131 + pattern := "%\"" + tag + "\"%" 132 + rows, err := db.Query(db.Rebind(` 133 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 134 + FROM bookmarks 135 + WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%' 136 + ORDER BY created_at DESC 137 + LIMIT ? OFFSET ? 138 + `), pattern, limit, offset) 139 + if err != nil { 140 + return nil, err 141 + } 142 + defer rows.Close() 143 + 144 + var bookmarks []Bookmark 145 + for rows.Next() { 146 + var b Bookmark 147 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 148 + return nil, err 149 + } 150 + bookmarks = append(bookmarks, b) 151 + } 152 + return bookmarks, nil 153 + } 154 + 155 + func (db *DB) GetSembleBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) { 156 + pattern := "%\"" + tag + "\"%" 157 + rows, err := db.Query(db.Rebind(` 158 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 159 + FROM bookmarks 160 + WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%' 63 161 ORDER BY created_at DESC 64 162 LIMIT ? OFFSET ? 65 163 `), pattern, limit, offset) ··· 104 202 return bookmarks, nil 105 203 } 106 204 205 + func (db *DB) GetMarginBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) { 206 + pattern := "%\"" + tag + "\"%" 207 + rows, err := db.Query(db.Rebind(` 208 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 209 + FROM bookmarks 210 + WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%' 211 + ORDER BY created_at DESC 212 + LIMIT ? OFFSET ? 213 + `), authorDID, pattern, limit, offset) 214 + if err != nil { 215 + return nil, err 216 + } 217 + defer rows.Close() 218 + 219 + var bookmarks []Bookmark 220 + for rows.Next() { 221 + var b Bookmark 222 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 223 + return nil, err 224 + } 225 + bookmarks = append(bookmarks, b) 226 + } 227 + return bookmarks, nil 228 + } 229 + 230 + func (db *DB) GetSembleBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) { 231 + pattern := "%\"" + tag + "\"%" 232 + rows, err := db.Query(db.Rebind(` 233 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 234 + FROM bookmarks 235 + WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%' 236 + ORDER BY created_at DESC 237 + LIMIT ? OFFSET ? 238 + `), authorDID, pattern, limit, offset) 239 + if err != nil { 240 + return nil, err 241 + } 242 + defer rows.Close() 243 + 244 + var bookmarks []Bookmark 245 + for rows.Next() { 246 + var b Bookmark 247 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 248 + return nil, err 249 + } 250 + bookmarks = append(bookmarks, b) 251 + } 252 + return bookmarks, nil 253 + } 254 + 107 255 func (db *DB) GetBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) { 108 256 rows, err := db.Query(db.Rebind(` 109 257 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 110 258 FROM bookmarks 111 259 WHERE author_did = ? 260 + ORDER BY created_at DESC 261 + LIMIT ? OFFSET ? 262 + `), authorDID, limit, offset) 263 + if err != nil { 264 + return nil, err 265 + } 266 + defer rows.Close() 267 + 268 + var bookmarks []Bookmark 269 + for rows.Next() { 270 + var b Bookmark 271 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 272 + return nil, err 273 + } 274 + bookmarks = append(bookmarks, b) 275 + } 276 + return bookmarks, nil 277 + } 278 + 279 + func (db *DB) GetMarginBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) { 280 + rows, err := db.Query(db.Rebind(` 281 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 282 + FROM bookmarks 283 + WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%' 284 + ORDER BY created_at DESC 285 + LIMIT ? OFFSET ? 286 + `), authorDID, limit, offset) 287 + if err != nil { 288 + return nil, err 289 + } 290 + defer rows.Close() 291 + 292 + var bookmarks []Bookmark 293 + for rows.Next() { 294 + var b Bookmark 295 + if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil { 296 + return nil, err 297 + } 298 + bookmarks = append(bookmarks, b) 299 + } 300 + return bookmarks, nil 301 + } 302 + 303 + func (db *DB) GetSembleBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) { 304 + rows, err := db.Query(db.Rebind(` 305 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 306 + FROM bookmarks 307 + WHERE author_did = ? AND uri LIKE '%network.cosmik%' 112 308 ORDER BY created_at DESC 113 309 LIMIT ? OFFSET ? 114 310 `), authorDID, limit, offset)
+196
backend/internal/db/queries_highlights.go
··· 55 55 return highlights, nil 56 56 } 57 57 58 + func (db *DB) GetMarginHighlights(limit, offset int) ([]Highlight, error) { 59 + rows, err := db.Query(db.Rebind(` 60 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 61 + FROM highlights 62 + WHERE uri NOT LIKE '%network.cosmik%' 63 + ORDER BY created_at DESC 64 + LIMIT ? OFFSET ? 65 + `), limit, offset) 66 + if err != nil { 67 + return nil, err 68 + } 69 + defer rows.Close() 70 + 71 + var highlights []Highlight 72 + for rows.Next() { 73 + var h Highlight 74 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 75 + return nil, err 76 + } 77 + highlights = append(highlights, h) 78 + } 79 + return highlights, nil 80 + } 81 + 82 + func (db *DB) GetSembleHighlights(limit, offset int) ([]Highlight, error) { 83 + rows, err := db.Query(db.Rebind(` 84 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 85 + FROM highlights 86 + WHERE uri LIKE '%network.cosmik%' 87 + ORDER BY created_at DESC 88 + LIMIT ? OFFSET ? 89 + `), limit, offset) 90 + if err != nil { 91 + return nil, err 92 + } 93 + defer rows.Close() 94 + 95 + var highlights []Highlight 96 + for rows.Next() { 97 + var h Highlight 98 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 99 + return nil, err 100 + } 101 + highlights = append(highlights, h) 102 + } 103 + return highlights, nil 104 + } 105 + 58 106 func (db *DB) GetHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) { 59 107 pattern := "%\"" + tag + "\"%" 60 108 rows, err := db.Query(db.Rebind(` 61 109 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 62 110 FROM highlights 63 111 WHERE tags_json LIKE ? 112 + ORDER BY created_at DESC 113 + LIMIT ? OFFSET ? 114 + `), pattern, limit, offset) 115 + if err != nil { 116 + return nil, err 117 + } 118 + defer rows.Close() 119 + 120 + var highlights []Highlight 121 + for rows.Next() { 122 + var h Highlight 123 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 124 + return nil, err 125 + } 126 + highlights = append(highlights, h) 127 + } 128 + return highlights, nil 129 + } 130 + 131 + func (db *DB) GetMarginHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) { 132 + pattern := "%\"" + tag + "\"%" 133 + rows, err := db.Query(db.Rebind(` 134 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 135 + FROM highlights 136 + WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%' 137 + ORDER BY created_at DESC 138 + LIMIT ? OFFSET ? 139 + `), pattern, limit, offset) 140 + if err != nil { 141 + return nil, err 142 + } 143 + defer rows.Close() 144 + 145 + var highlights []Highlight 146 + for rows.Next() { 147 + var h Highlight 148 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 149 + return nil, err 150 + } 151 + highlights = append(highlights, h) 152 + } 153 + return highlights, nil 154 + } 155 + 156 + func (db *DB) GetSembleHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) { 157 + pattern := "%\"" + tag + "\"%" 158 + rows, err := db.Query(db.Rebind(` 159 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 160 + FROM highlights 161 + WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%' 64 162 ORDER BY created_at DESC 65 163 LIMIT ? OFFSET ? 66 164 `), pattern, limit, offset) ··· 105 203 return highlights, nil 106 204 } 107 205 206 + func (db *DB) GetMarginHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) { 207 + pattern := "%\"" + tag + "\"%" 208 + rows, err := db.Query(db.Rebind(` 209 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 210 + FROM highlights 211 + WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%' 212 + ORDER BY created_at DESC 213 + LIMIT ? OFFSET ? 214 + `), authorDID, pattern, limit, offset) 215 + if err != nil { 216 + return nil, err 217 + } 218 + defer rows.Close() 219 + 220 + var highlights []Highlight 221 + for rows.Next() { 222 + var h Highlight 223 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 224 + return nil, err 225 + } 226 + highlights = append(highlights, h) 227 + } 228 + return highlights, nil 229 + } 230 + 231 + func (db *DB) GetSembleHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) { 232 + pattern := "%\"" + tag + "\"%" 233 + rows, err := db.Query(db.Rebind(` 234 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 235 + FROM highlights 236 + WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%' 237 + ORDER BY created_at DESC 238 + LIMIT ? OFFSET ? 239 + `), authorDID, pattern, limit, offset) 240 + if err != nil { 241 + return nil, err 242 + } 243 + defer rows.Close() 244 + 245 + var highlights []Highlight 246 + for rows.Next() { 247 + var h Highlight 248 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 249 + return nil, err 250 + } 251 + highlights = append(highlights, h) 252 + } 253 + return highlights, nil 254 + } 255 + 108 256 func (db *DB) GetHighlightsByTargetHash(targetHash string, limit, offset int) ([]Highlight, error) { 109 257 rows, err := db.Query(db.Rebind(` 110 258 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid ··· 134 282 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 135 283 FROM highlights 136 284 WHERE author_did = ? 285 + ORDER BY created_at DESC 286 + LIMIT ? OFFSET ? 287 + `), authorDID, limit, offset) 288 + if err != nil { 289 + return nil, err 290 + } 291 + defer rows.Close() 292 + 293 + var highlights []Highlight 294 + for rows.Next() { 295 + var h Highlight 296 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 297 + return nil, err 298 + } 299 + highlights = append(highlights, h) 300 + } 301 + return highlights, nil 302 + } 303 + 304 + func (db *DB) GetMarginHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) { 305 + rows, err := db.Query(db.Rebind(` 306 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 307 + FROM highlights 308 + WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%' 309 + ORDER BY created_at DESC 310 + LIMIT ? OFFSET ? 311 + `), authorDID, limit, offset) 312 + if err != nil { 313 + return nil, err 314 + } 315 + defer rows.Close() 316 + 317 + var highlights []Highlight 318 + for rows.Next() { 319 + var h Highlight 320 + if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil { 321 + return nil, err 322 + } 323 + highlights = append(highlights, h) 324 + } 325 + return highlights, nil 326 + } 327 + 328 + func (db *DB) GetSembleHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) { 329 + rows, err := db.Query(db.Rebind(` 330 + SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid 331 + FROM highlights 332 + WHERE author_did = ? AND uri LIKE '%network.cosmik%' 137 333 ORDER BY created_at DESC 138 334 LIMIT ? OFFSET ? 139 335 `), authorDID, limit, offset)
+3 -2
backend/internal/oauth/client.go
··· 18 18 19 19 "github.com/go-jose/go-jose/v4" 20 20 "github.com/go-jose/go-jose/v4/jwt" 21 + "margin.at/internal/config" 21 22 ) 22 23 23 24 type Client struct { ··· 86 87 } 87 88 88 89 func (c *Client) ResolveHandle(ctx context.Context, handle string) (string, error) { 89 - did, err := c.resolveHandleAt(ctx, handle, "https://public.api.bsky.app") 90 + did, err := c.resolveHandleAt(ctx, handle, config.Get().BskyPublicAPI) 90 91 if err == nil { 91 92 return did, nil 92 93 } ··· 140 141 func (c *Client) ResolveDIDToPDS(ctx context.Context, did string) (string, error) { 141 142 var docURL string 142 143 if strings.HasPrefix(did, "did:plc:") { 143 - docURL = fmt.Sprintf("https://plc.directory/%s", did) 144 + docURL = config.Get().PLCResolveURL(did) 144 145 } else if strings.HasPrefix(did, "did:web:") { 145 146 domain := strings.TrimPrefix(did, "did:web:") 146 147 docURL = fmt.Sprintf("https://%s/.well-known/did.json", domain)
+3 -15
backend/internal/oauth/handler.go
··· 184 184 } 185 185 186 186 var req struct { 187 - Handle string `json:"handle"` 188 - InviteCode string `json:"invite_code"` 187 + Handle string `json:"handle"` 189 188 } 190 189 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 191 190 http.Error(w, "Invalid request body", http.StatusBadRequest) ··· 194 193 195 194 if req.Handle == "" { 196 195 http.Error(w, "Handle is required", http.StatusBadRequest) 197 - return 198 - } 199 - 200 - requiredCode := os.Getenv("INVITE_CODE") 201 - if requiredCode != "" && req.InviteCode != requiredCode { 202 - w.Header().Set("Content-Type", "application/json") 203 - w.WriteHeader(http.StatusForbidden) 204 - json.NewEncoder(w).Encode(map[string]string{ 205 - "error": "Invite code required", 206 - "code": "invite_required", 207 - }) 208 196 return 209 197 } 210 198 ··· 457 445 Path: "/", 458 446 HttpOnly: true, 459 447 Secure: true, 460 - SameSite: http.SameSiteNoneMode, 448 + SameSite: http.SameSiteLaxMode, 461 449 MaxAge: 86400 * 7, 462 450 }) 463 451 ··· 536 524 func deleteFromPDS(pds, accessToken string, dpopKey *ecdsa.PrivateKey, collection, did, rkey string) { 537 525 538 526 client := xrpc.NewClient(pds, accessToken, dpopKey) 539 - err := client.DeleteRecord(context.Background(), collection, did, rkey) 527 + err := client.DeleteRecord(context.Background(), did, collection, rkey) 540 528 if err != nil { 541 529 log.Printf("Failed to delete orphaned reply from PDS: %v", err) 542 530 } else {
+6
backend/internal/xrpc/records.go
··· 242 242 } 243 243 244 244 func (r *ReplyRecord) Validate() error { 245 + if r.Parent.URI == "" || r.Parent.CID == "" { 246 + return fmt.Errorf("parent uri and cid are required") 247 + } 248 + if r.Root.URI == "" || r.Root.CID == "" { 249 + return fmt.Errorf("root uri and cid are required") 250 + } 245 251 if r.Text == "" { 246 252 return fmt.Errorf("text is required") 247 253 }
+3 -2
backend/internal/xrpc/utils.go
··· 10 10 "strings" 11 11 "time" 12 12 13 + "margin.at/internal/config" 13 14 "margin.at/internal/slingshot" 14 15 ) 15 16 ··· 100 101 func resolveDIDToPDSDirect(did string) (string, error) { 101 102 var docURL string 102 103 if strings.HasPrefix(did, "did:plc:") { 103 - docURL = fmt.Sprintf("https://plc.directory/%s", did) 104 + docURL = config.Get().PLCResolveURL(did) 104 105 } else if strings.HasPrefix(did, "did:web:") { 105 106 domain := strings.TrimPrefix(did, "did:web:") 106 107 docURL = fmt.Sprintf("https://%s/.well-known/did.json", domain) ··· 161 162 } 162 163 163 164 func resolveHandleDirect(handle string) (string, error) { 164 - url := fmt.Sprintf("https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=%s", handle) 165 + url := config.Get().BskyResolveHandleURL(handle) 165 166 client := &http.Client{ 166 167 Timeout: 5 * time.Second, 167 168 }
+2 -30
web/src/api/client.js
··· 470 470 return data.did; 471 471 } 472 472 473 - export async function startLogin(handle, inviteCode) { 473 + export async function startLogin(handle) { 474 474 return request(`${AUTH_BASE}/start`, { 475 475 method: "POST", 476 - body: JSON.stringify({ handle, invite_code: inviteCode }), 476 + body: JSON.stringify({ handle }), 477 477 }); 478 478 } 479 479 ··· 502 502 return request(`${API_BASE}/keys/${id}`, { method: "DELETE" }); 503 503 } 504 504 505 - export async function describeServer(service) { 506 - const res = await fetch(`${service}/xrpc/com.atproto.server.describeServer`); 507 - if (!res.ok) throw new Error("Failed to describe server"); 508 - return res.json(); 509 - } 510 505 511 - export async function createAccount( 512 - service, 513 - { handle, email, password, inviteCode }, 514 - ) { 515 - const res = await fetch(`${service}/xrpc/com.atproto.server.createAccount`, { 516 - method: "POST", 517 - headers: { 518 - "Content-Type": "application/json", 519 - }, 520 - body: JSON.stringify({ 521 - handle, 522 - email, 523 - password, 524 - inviteCode, 525 - }), 526 - }); 527 - 528 - const data = await res.json(); 529 - if (!res.ok) { 530 - throw new Error(data.message || data.error || "Failed to create account"); 531 - } 532 - return data; 533 - }
+5 -38
web/src/pages/Login.jsx
··· 9 9 const { isAuthenticated, user, logout } = useAuth(); 10 10 const [showSignUp, setShowSignUp] = useState(false); 11 11 const [handle, setHandle] = useState(""); 12 - const [inviteCode, setInviteCode] = useState(""); 13 - const [showInviteInput, setShowInviteInput] = useState(false); 14 12 const [suggestions, setSuggestions] = useState([]); 15 13 const [showSuggestions, setShowSuggestions] = useState(false); 16 14 const [loading, setLoading] = useState(false); 17 15 const [error, setError] = useState(null); 18 16 const [selectedIndex, setSelectedIndex] = useState(-1); 19 17 const inputRef = useRef(null); 20 - const inviteRef = useRef(null); 21 18 const suggestionsRef = useRef(null); 22 19 23 20 const [providerIndex, setProviderIndex] = useState(0); ··· 143 140 const handleSubmit = async (e) => { 144 141 e.preventDefault(); 145 142 if (!handle.trim()) return; 146 - if (showInviteInput && !inviteCode.trim()) return; 147 143 148 144 setLoading(true); 149 145 setError(null); 150 146 151 147 try { 152 - const result = await startLogin(handle.trim(), inviteCode.trim()); 148 + const result = await startLogin(handle.trim()); 153 149 if (result.authorizationUrl) { 154 150 window.location.href = result.authorizationUrl; 155 151 } 156 152 } catch (err) { 157 153 console.error("Login error:", err); 158 - if ( 159 - err.message && 160 - (err.message.includes("invite_required") || 161 - err.message.includes("Invite code required")) 162 - ) { 163 - setShowInviteInput(true); 164 - setError("Please enter an invite code to continue."); 165 - setTimeout(() => inviteRef.current?.focus(), 100); 166 - } else { 167 - setError(err.message || "Failed to start login"); 168 - } 154 + setError(err.message || "Failed to start login"); 169 155 setLoading(false); 170 156 } 171 157 }; ··· 261 247 )} 262 248 </div> 263 249 264 - {showInviteInput && ( 265 - <div 266 - className="login-input-wrapper" 267 - style={{ marginTop: "12px", animation: "fadeIn 0.3s ease" }} 268 - > 269 - <input 270 - ref={inviteRef} 271 - type="text" 272 - className="login-input" 273 - placeholder="Enter invite code" 274 - value={inviteCode} 275 - onChange={(e) => setInviteCode(e.target.value)} 276 - autoComplete="off" 277 - disabled={loading} 278 - style={{ borderColor: "var(--accent)" }} 279 - /> 280 - </div> 281 - )} 250 + 282 251 283 252 {error && <p className="login-error">{error}</p>} 284 253 ··· 286 255 type="submit" 287 256 className="btn btn-primary login-submit" 288 257 disabled={ 289 - loading || !handle.trim() || (showInviteInput && !inviteCode.trim()) 258 + loading || !handle.trim() 290 259 } 291 260 > 292 261 {loading 293 262 ? "Connecting..." 294 - : showInviteInput 295 - ? "Submit Code" 296 - : "Continue"} 263 + : "Continue"} 297 264 </button> 298 265 299 266 <p className="login-legal">