(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

backend improvements

scanash00 9a4df148 3ec15276

+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">