Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

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

appview/oauth: add users to default knot/spindle upon login

Signed-off-by: oppiliappan <me@oppi.li>

+217 -12
+196
appview/oauth/handler.go
··· 1 1 package oauth 2 2 3 3 import ( 4 + "bytes" 5 + "context" 4 6 "encoding/json" 7 + "fmt" 5 8 "log" 6 9 "net/http" 10 + "slices" 11 + "time" 7 12 8 13 "github.com/go-chi/chi/v5" 9 14 "github.com/lestrrat-go/jwx/v2/jwk" 10 15 "github.com/posthog/posthog-go" 16 + "tangled.org/core/api/tangled" 17 + "tangled.org/core/appview/db" 18 + "tangled.org/core/consts" 19 + "tangled.org/core/tid" 11 20 ) 12 21 13 22 func (o *OAuth) Router() http.Handler { ··· 71 62 return 72 63 } 73 64 65 + log.Println("session saved successfully") 66 + go o.addToDefaultKnot(sessData.AccountDID.String()) 67 + go o.addToDefaultSpindle(sessData.AccountDID.String()) 68 + 74 69 if !o.Config.Core.Dev { 75 70 err = o.Posthog.Enqueue(posthog.Capture{ 76 71 DistinctId: sessData.AccountDID.String(), ··· 86 73 } 87 74 88 75 http.Redirect(w, r, "/", http.StatusFound) 76 + } 77 + 78 + func (o *OAuth) addToDefaultSpindle(did string) { 79 + // use the tangled.sh app password to get an accessJwt 80 + // and create an sh.tangled.spindle.member record with that 81 + spindleMembers, err := db.GetSpindleMembers( 82 + o.Db, 83 + db.FilterEq("instance", "spindle.tangled.sh"), 84 + db.FilterEq("subject", did), 85 + ) 86 + if err != nil { 87 + log.Printf("failed to get spindle members for did %s: %v", did, err) 88 + return 89 + } 90 + 91 + if len(spindleMembers) != 0 { 92 + log.Printf("did %s is already a member of the default spindle", did) 93 + return 94 + } 95 + 96 + log.Printf("adding %s to default spindle", did) 97 + session, err := o.createAppPasswordSession(o.Config.Core.AppPassword, consts.TangledDid) 98 + if err != nil { 99 + log.Printf("failed to create session: %s", err) 100 + return 101 + } 102 + 103 + record := tangled.SpindleMember{ 104 + LexiconTypeID: "sh.tangled.spindle.member", 105 + Subject: did, 106 + Instance: consts.DefaultSpindle, 107 + CreatedAt: time.Now().Format(time.RFC3339), 108 + } 109 + 110 + if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { 111 + log.Printf("failed to add member to default spindle: %s", err) 112 + return 113 + } 114 + 115 + log.Printf("successfully added %s to default spindle", did) 116 + } 117 + 118 + func (o *OAuth) addToDefaultKnot(did string) { 119 + // use the tangled.sh app password to get an accessJwt 120 + // and create an sh.tangled.spindle.member record with that 121 + 122 + allKnots, err := o.Enforcer.GetKnotsForUser(did) 123 + if err != nil { 124 + log.Printf("failed to get knot members for did %s: %v", did, err) 125 + return 126 + } 127 + 128 + if slices.Contains(allKnots, consts.DefaultKnot) { 129 + log.Printf("did %s is already a member of the default knot", did) 130 + return 131 + } 132 + 133 + log.Printf("adding %s to default knot", did) 134 + session, err := o.createAppPasswordSession(o.Config.Core.TmpAltAppPassword, consts.IcyDid) 135 + if err != nil { 136 + log.Printf("failed to create session: %s", err) 137 + return 138 + } 139 + 140 + record := tangled.KnotMember{ 141 + LexiconTypeID: "sh.tangled.knot.member", 142 + Subject: did, 143 + Domain: consts.DefaultKnot, 144 + CreatedAt: time.Now().Format(time.RFC3339), 145 + } 146 + 147 + if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil { 148 + log.Printf("failed to add member to default knot: %s", err) 149 + return 150 + } 151 + 152 + if err := o.Enforcer.AddKnotMember(consts.DefaultKnot, did); err != nil { 153 + log.Printf("failed to set up enforcer rules: %s", err) 154 + return 155 + } 156 + 157 + log.Printf("successfully added %s to default Knot", did) 158 + } 159 + 160 + // create a session using apppasswords 161 + type session struct { 162 + AccessJwt string `json:"accessJwt"` 163 + PdsEndpoint string 164 + Did string 165 + } 166 + 167 + func (o *OAuth) createAppPasswordSession(appPassword, did string) (*session, error) { 168 + if appPassword == "" { 169 + return nil, fmt.Errorf("no app password configured, skipping member addition") 170 + } 171 + 172 + resolved, err := o.IdResolver.ResolveIdent(context.Background(), did) 173 + if err != nil { 174 + return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", did, err) 175 + } 176 + 177 + pdsEndpoint := resolved.PDSEndpoint() 178 + if pdsEndpoint == "" { 179 + return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", did) 180 + } 181 + 182 + sessionPayload := map[string]string{ 183 + "identifier": did, 184 + "password": appPassword, 185 + } 186 + sessionBytes, err := json.Marshal(sessionPayload) 187 + if err != nil { 188 + return nil, fmt.Errorf("failed to marshal session payload: %v", err) 189 + } 190 + 191 + sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession" 192 + sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes)) 193 + if err != nil { 194 + return nil, fmt.Errorf("failed to create session request: %v", err) 195 + } 196 + sessionReq.Header.Set("Content-Type", "application/json") 197 + 198 + client := &http.Client{Timeout: 30 * time.Second} 199 + sessionResp, err := client.Do(sessionReq) 200 + if err != nil { 201 + return nil, fmt.Errorf("failed to create session: %v", err) 202 + } 203 + defer sessionResp.Body.Close() 204 + 205 + if sessionResp.StatusCode != http.StatusOK { 206 + return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode) 207 + } 208 + 209 + var session session 210 + if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil { 211 + return nil, fmt.Errorf("failed to decode session response: %v", err) 212 + } 213 + 214 + session.PdsEndpoint = pdsEndpoint 215 + session.Did = did 216 + 217 + return &session, nil 218 + } 219 + 220 + func (s *session) putRecord(record any, collection string) error { 221 + recordBytes, err := json.Marshal(record) 222 + if err != nil { 223 + return fmt.Errorf("failed to marshal knot member record: %w", err) 224 + } 225 + 226 + payload := map[string]any{ 227 + "repo": s.Did, 228 + "collection": collection, 229 + "rkey": tid.TID(), 230 + "record": json.RawMessage(recordBytes), 231 + } 232 + 233 + payloadBytes, err := json.Marshal(payload) 234 + if err != nil { 235 + return fmt.Errorf("failed to marshal request payload: %w", err) 236 + } 237 + 238 + url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord" 239 + req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes)) 240 + if err != nil { 241 + return fmt.Errorf("failed to create HTTP request: %w", err) 242 + } 243 + 244 + req.Header.Set("Content-Type", "application/json") 245 + req.Header.Set("Authorization", "Bearer "+s.AccessJwt) 246 + 247 + client := &http.Client{Timeout: 30 * time.Second} 248 + resp, err := client.Do(req) 249 + if err != nil { 250 + return fmt.Errorf("failed to add user to default service: %w", err) 251 + } 252 + defer resp.Body.Close() 253 + 254 + if resp.StatusCode != http.StatusOK { 255 + return fmt.Errorf("failed to add user to default service: HTTP %d", resp.StatusCode) 256 + } 257 + 258 + return nil 89 259 }
+20 -11
appview/oauth/oauth.go
··· 15 15 "github.com/lestrrat-go/jwx/v2/jwk" 16 16 "github.com/posthog/posthog-go" 17 17 "tangled.org/core/appview/config" 18 + "tangled.org/core/appview/db" 19 + "tangled.org/core/idresolver" 20 + "tangled.org/core/rbac" 18 21 ) 19 22 20 - func New(config *config.Config, ph posthog.Client) (*OAuth, error) { 23 + func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enforcer, res *idresolver.Resolver) (*OAuth, error) { 21 24 22 25 var oauthConfig oauth.ClientConfig 23 26 var clientUri string ··· 46 43 sessStore := sessions.NewCookieStore([]byte(config.Core.CookieSecret)) 47 44 48 45 return &OAuth{ 49 - ClientApp: oauth.NewClientApp(&oauthConfig, authStore), 50 - Config: config, 51 - SessStore: sessStore, 52 - JwksUri: jwksUri, 53 - Posthog: ph, 46 + ClientApp: oauth.NewClientApp(&oauthConfig, authStore), 47 + Config: config, 48 + SessStore: sessStore, 49 + JwksUri: jwksUri, 50 + Posthog: ph, 51 + Db: db, 52 + Enforcer: enforcer, 53 + IdResolver: res, 54 54 }, nil 55 55 } 56 56 57 57 type OAuth struct { 58 - ClientApp *oauth.ClientApp 59 - SessStore *sessions.CookieStore 60 - Config *config.Config 61 - JwksUri string 62 - Posthog posthog.Client 58 + ClientApp *oauth.ClientApp 59 + SessStore *sessions.CookieStore 60 + Config *config.Config 61 + JwksUri string 62 + Posthog posthog.Client 63 + Db *db.DB 64 + Enforcer *rbac.Enforcer 65 + IdResolver *idresolver.Resolver 63 66 } 64 67 65 68 func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error {
+1 -1
appview/state/state.go
··· 85 85 pages := pages.NewPages(config, res) 86 86 cache := cache.New(config.Redis.Addr) 87 87 sess := session.New(cache) 88 - oauth2, err := oauth.New(config, posthog) 88 + oauth2, err := oauth.New(config, posthog, d, enforcer, res) 89 89 if err != nil { 90 90 return nil, fmt.Errorf("failed to start oauth handler: %w", err) 91 91 }