···79798080// RepositoryStats represents statistics for a repository
8181type RepositoryStats struct {
8282- DID string
8383- Repository string
8484- StarCount int
8585- PullCount int
8686- LastPull *time.Time
8787- PushCount int
8888- LastPush *time.Time
8282+ DID string `json:"did"`
8383+ Repository string `json:"repository"`
8484+ StarCount int `json:"star_count"`
8585+ PullCount int `json:"pull_count"`
8686+ LastPull *time.Time `json:"last_pull,omitempty"`
8787+ PushCount int `json:"push_count"`
8888+ LastPush *time.Time `json:"last_push,omitempty"`
8989}
+19
pkg/appview/db/oauth_store.go
···9292 return err
9393}
94949595+// DeleteSessionsForDID removes all sessions for a given DID
9696+// This is useful for logout flows where we want to revoke all OAuth sessions
9797+func (s *OAuthStore) DeleteSessionsForDID(ctx context.Context, did string) error {
9898+ result, err := s.db.ExecContext(ctx, `
9999+ DELETE FROM oauth_sessions WHERE account_did = ?
100100+ `, did)
101101+102102+ if err != nil {
103103+ return fmt.Errorf("failed to delete sessions for DID: %w", err)
104104+ }
105105+106106+ deleted, _ := result.RowsAffected()
107107+ if deleted > 0 {
108108+ fmt.Printf("Deleted %d OAuth session(s) for DID %s\n", deleted, did)
109109+ }
110110+111111+ return nil
112112+}
113113+95114// GetAuthRequestInfo retrieves authentication request data by state
96115func (s *OAuthStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) {
97116 var requestDataJSON string
-19
pkg/appview/db/schema.go
···174174 return nil, err
175175 }
176176177177- // Migration: Drop raw_manifest column if it exists
178178- // Check if column exists first
179179- var columnExists bool
180180- err = db.QueryRow(`
181181- SELECT COUNT(*) > 0
182182- FROM pragma_table_info('manifests')
183183- WHERE name = 'raw_manifest'
184184- `).Scan(&columnExists)
185185- if err != nil {
186186- return nil, err
187187- }
188188-189189- if columnExists {
190190- // Drop the column (requires SQLite 3.35.0+)
191191- if _, err := db.Exec(`ALTER TABLE manifests DROP COLUMN raw_manifest`); err != nil {
192192- return nil, err
193193- }
194194- }
195195-196177 return db, nil
197178}
+34-7
pkg/appview/handlers/api.go
···44 "context"
55 "database/sql"
66 "encoding/json"
77+ "fmt"
88+ "log"
79 "net/http"
810911 "atcr.io/pkg/appview/db"
···3840 // Resolve owner's handle to DID
3941 ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
4042 if err != nil {
4141- http.Error(w, "Failed to resolve handle", http.StatusBadRequest)
4343+ log.Printf("StarRepository: Failed to resolve handle %s: %v", handle, err)
4444+ http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
4245 return
4346 }
4747+ log.Printf("StarRepository: Resolved %s to DID %s", handle, ownerDID)
44484549 // Get OAuth session for the authenticated user
5050+ log.Printf("StarRepository: Getting OAuth session for user DID %s", user.DID)
4651 session, err := h.Refresher.GetSession(r.Context(), user.DID)
4752 if err != nil {
4848- http.Error(w, "Failed to get OAuth session", http.StatusUnauthorized)
5353+ log.Printf("StarRepository: Failed to get OAuth session for %s: %v", user.DID, err)
5454+ http.Error(w, fmt.Sprintf("Failed to get OAuth session: %v", err), http.StatusUnauthorized)
4955 return
5056 }
5757+ log.Printf("StarRepository: Got OAuth session for %s", user.DID)
51585259 // Get user's PDS client (use indigo's API client which handles DPoP automatically)
5360 apiClient := session.APIClient()
···5764 starRecord := atproto.NewStarRecord(ownerDID, repository)
5865 rkey := atproto.StarRecordKey(ownerDID, repository)
59666767+ log.Printf("StarRepository: Creating star record for %s/%s (rkey: %s)", handle, repository, rkey)
6868+6069 // Write star record to user's PDS
6170 _, err = pdsClient.PutRecord(r.Context(), atproto.StarCollection, rkey, starRecord)
6271 if err != nil {
6363- http.Error(w, "Failed to create star", http.StatusInternalServerError)
7272+ log.Printf("StarRepository: Failed to create star record: %v", err)
7373+ http.Error(w, fmt.Sprintf("Failed to create star: %v", err), http.StatusInternalServerError)
6474 return
6575 }
7676+7777+ log.Printf("StarRepository: Successfully starred %s/%s", handle, repository)
66786779 // Return success
6880 w.Header().Set("Content-Type", "application/json")
···93105 // Resolve owner's handle to DID
94106 ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
95107 if err != nil {
9696- http.Error(w, "Failed to resolve handle", http.StatusBadRequest)
108108+ log.Printf("UnstarRepository: Failed to resolve handle %s: %v", handle, err)
109109+ http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
97110 return
98111 }
112112+ log.Printf("UnstarRepository: Resolved %s to DID %s", handle, ownerDID)
99113100114 // Get OAuth session for the authenticated user
115115+ log.Printf("UnstarRepository: Getting OAuth session for user DID %s", user.DID)
101116 session, err := h.Refresher.GetSession(r.Context(), user.DID)
102117 if err != nil {
103103- http.Error(w, "Failed to get OAuth session", http.StatusUnauthorized)
118118+ log.Printf("UnstarRepository: Failed to get OAuth session for %s: %v", user.DID, err)
119119+ http.Error(w, fmt.Sprintf("Failed to get OAuth session: %v", err), http.StatusUnauthorized)
104120 return
105121 }
122122+ log.Printf("UnstarRepository: Got OAuth session for %s", user.DID)
106123107124 // Get user's PDS client (use indigo's API client which handles DPoP automatically)
108125 apiClient := session.APIClient()
···110127111128 // Delete star record from user's PDS
112129 rkey := atproto.StarRecordKey(ownerDID, repository)
130130+ log.Printf("UnstarRepository: Deleting star record for %s/%s (rkey: %s)", handle, repository, rkey)
113131 err = pdsClient.DeleteRecord(r.Context(), atproto.StarCollection, rkey)
114132 if err != nil {
115133 // If record doesn't exist, still return success (idempotent)
116134 if err.Error() != "record not found" {
117117- http.Error(w, "Failed to delete star", http.StatusInternalServerError)
135135+ log.Printf("UnstarRepository: Failed to delete star record: %v", err)
136136+ http.Error(w, fmt.Sprintf("Failed to delete star: %v", err), http.StatusInternalServerError)
118137 return
119138 }
139139+ log.Printf("UnstarRepository: Star record not found (already unstarred)")
140140+ } else {
141141+ log.Printf("UnstarRepository: Successfully unstarred %s/%s", handle, repository)
120142 }
121143122144 // Return success
···149171 // Resolve owner's handle to DID
150172 ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle)
151173 if err != nil {
152152- http.Error(w, "Failed to resolve handle", http.StatusBadRequest)
174174+ log.Printf("CheckStar: Failed to resolve handle %s: %v", handle, err)
175175+ http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest)
153176 return
154177 }
155178156179 // Get OAuth session for the authenticated user
180180+ log.Printf("CheckStar: Getting OAuth session for user DID %s", user.DID)
157181 session, err := h.Refresher.GetSession(r.Context(), user.DID)
158182 if err != nil {
183183+ log.Printf("CheckStar: Failed to get OAuth session for %s: %v", user.DID, err)
159184 // No OAuth session - return not starred
160185 w.Header().Set("Content-Type", "application/json")
161186 json.NewEncoder(w).Encode(map[string]bool{"starred": false})
···168193169194 // Check if star record exists
170195 rkey := atproto.StarRecordKey(ownerDID, repository)
196196+ log.Printf("CheckStar: Checking star record for %s/%s (rkey: %s)", handle, repository, rkey)
171197 _, err = pdsClient.GetRecord(r.Context(), atproto.StarCollection, rkey)
172198173199 starred := err == nil
200200+ log.Printf("CheckStar: Star status for %s/%s: %v (err: %v)", handle, repository, starred, err)
174201175202 // Return result
176203 w.Header().Set("Content-Type", "application/json")
+28-3
pkg/appview/handlers/auth.go
···7676 did := ident.DID.String()
77777878 // Try to get existing OAuth session
7979- _, err := h.Refresher.GetSession(r.Context(), did)
7979+ session, err := h.Refresher.GetSession(r.Context(), did)
8080 if err == nil {
8181- // Found valid OAuth session! Create UI session silently
8282- fmt.Printf("DEBUG [auth]: Silent login successful for %s (DID: %s)\n", handle, did)
8181+ // Check if the session has all required scopes
8282+ requiredScopes := oauth.GetDefaultScopes()
8383+ sessionScopes := session.Data.Scopes
8484+8585+ if !hasAllScopes(sessionScopes, requiredScopes) {
8686+ fmt.Printf("DEBUG [auth]: Session scopes mismatch for %s. Required: %v, Have: %v. Forcing re-auth.\n",
8787+ handle, requiredScopes, sessionScopes)
8888+ } else {
8989+ // Found valid OAuth session with all required scopes! Create UI session silently
9090+ fmt.Printf("DEBUG [auth]: Silent login successful for %s (DID: %s)\n", handle, did)
83918492 // Get PDS endpoint from identity
8593 pdsEndpoint := ident.PDSEndpoint()
···107115 }
108116109117 fmt.Printf("WARNING [auth]: Failed to create UI session during silent login: %v\n", err)
118118+ }
110119 } else {
111120 fmt.Printf("DEBUG [auth]: No valid OAuth session found for %s: %v\n", handle, err)
112121 }
···135144 // Redirect to OAuth authorize with handle
136145 http.Redirect(w, r, "/auth/oauth/authorize?handle="+handle, http.StatusFound)
137146}
147147+148148+// hasAllScopes checks if grantedScopes contains all requiredScopes
149149+func hasAllScopes(grantedScopes, requiredScopes []string) bool {
150150+ grantedSet := make(map[string]bool)
151151+ for _, scope := range grantedScopes {
152152+ grantedSet[scope] = true
153153+ }
154154+155155+ for _, required := range requiredScopes {
156156+ if !grantedSet[required] {
157157+ return false
158158+ }
159159+ }
160160+161161+ return true
162162+}