Skip to content

Commit

Permalink
Implement strava oauth2 as new auth provider
Browse files Browse the repository at this point in the history
  • Loading branch information
szsascha committed Dec 31, 2022
1 parent 8f6cb5a commit d710446
Show file tree
Hide file tree
Showing 35 changed files with 137 additions and 34 deletions.
3 changes: 3 additions & 0 deletions apis/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func TestSettingsList(t *testing.T) {
`"spotifyAuth":{`,
`"kakaoAuth":{`,
`"twitchAuth":{`,
`"stravaAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
},
Expand Down Expand Up @@ -125,6 +126,7 @@ func TestSettingsSet(t *testing.T) {
`"spotifyAuth":{`,
`"kakaoAuth":{`,
`"twitchAuth":{`,
`"stravaAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"acme_test"`,
Expand Down Expand Up @@ -181,6 +183,7 @@ func TestSettingsSet(t *testing.T) {
`"spotifyAuth":{`,
`"kakaoAuth":{`,
`"twitchAuth":{`,
`"stravaAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"update_test"`,
Expand Down
7 changes: 7 additions & 0 deletions models/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Settings struct {
SpotifyAuth AuthProviderConfig `form:"spotifyAuth" json:"spotifyAuth"`
KakaoAuth AuthProviderConfig `form:"kakaoAuth" json:"kakaoAuth"`
TwitchAuth AuthProviderConfig `form:"twitchAuth" json:"twitchAuth"`
StravaAuth AuthProviderConfig `form:"stravaAuth" json:"stravaAuth"`
}

// New creates and returns a new default Settings instance.
Expand Down Expand Up @@ -124,6 +125,9 @@ func New() *Settings {
TwitchAuth: AuthProviderConfig{
Enabled: false,
},
StravaAuth: AuthProviderConfig{
Enabled: false,
},
}
}

Expand Down Expand Up @@ -153,6 +157,7 @@ func (s *Settings) Validate() error {
validation.Field(&s.SpotifyAuth),
validation.Field(&s.KakaoAuth),
validation.Field(&s.TwitchAuth),
validation.Field(&s.StravaAuth),
)
}

Expand Down Expand Up @@ -207,6 +212,7 @@ func (s *Settings) RedactClone() (*Settings, error) {
&clone.SpotifyAuth.ClientSecret,
&clone.KakaoAuth.ClientSecret,
&clone.TwitchAuth.ClientSecret,
&clone.StravaAuth.ClientSecret,
}

// mask all sensitive fields
Expand Down Expand Up @@ -236,6 +242,7 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
auth.NameSpotify: s.SpotifyAuth,
auth.NameKakao: s.KakaoAuth,
auth.NameTwitch: s.TwitchAuth,
auth.NameStrava: s.StravaAuth,
}
}

Expand Down
10 changes: 9 additions & 1 deletion models/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func TestSettingsValidate(t *testing.T) {
s.KakaoAuth.ClientId = ""
s.TwitchAuth.Enabled = true
s.TwitchAuth.ClientId = ""
s.StravaAuth.Enabled = true
s.StravaAuth.ClientId = ""

// check if Validate() is triggering the members validate methods.
err := s.Validate()
Expand Down Expand Up @@ -76,6 +78,7 @@ func TestSettingsValidate(t *testing.T) {
`"spotifyAuth":{`,
`"kakaoAuth":{`,
`"twitchAuth":{`,
`"stravaAuth":{`,
}

errBytes, _ := json.Marshal(err)
Expand Down Expand Up @@ -124,6 +127,8 @@ func TestSettingsMerge(t *testing.T) {
s2.KakaoAuth.ClientId = "kakao_test"
s2.TwitchAuth.Enabled = true
s2.TwitchAuth.ClientId = "twitch_test"
s2.StravaAuth.Enabled = true
s2.StravaAuth.ClientId = "strava_test"

if err := s1.Merge(s2); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -195,6 +200,7 @@ func TestSettingsRedactClone(t *testing.T) {
s1.SpotifyAuth.ClientSecret = "test123"
s1.KakaoAuth.ClientSecret = "test123"
s1.TwitchAuth.ClientSecret = "test123"
s1.StravaAuth.ClientSecret = "test123"

s2, err := s1.RedactClone()
if err != nil {
Expand All @@ -206,7 +212,7 @@ func TestSettingsRedactClone(t *testing.T) {
t.Fatal(err)
}

expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"[email protected]","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":5},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","authMethod":"","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"recordAuthToken":{"secret":"******","duration":1209600},"recordPasswordResetToken":{"secret":"******","duration":1800},"recordEmailChangeToken":{"secret":"******","duration":1800},"recordVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":false,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":0},"googleAuth":{"enabled":false,"clientSecret":"******"},"facebookAuth":{"enabled":false,"clientSecret":"******"},"githubAuth":{"enabled":false,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"clientSecret":"******"},"discordAuth":{"enabled":false,"clientSecret":"******"},"twitterAuth":{"enabled":false,"clientSecret":"******"},"microsoftAuth":{"enabled":false,"clientSecret":"******"},"spotifyAuth":{"enabled":false,"clientSecret":"******"},"kakaoAuth":{"enabled":false,"clientSecret":"******"},"twitchAuth":{"enabled":false,"clientSecret":"******"}}`
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"[email protected]","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":5},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","authMethod":"","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"recordAuthToken":{"secret":"******","duration":1209600},"recordPasswordResetToken":{"secret":"******","duration":1800},"recordEmailChangeToken":{"secret":"******","duration":1800},"recordVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":false,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":0},"googleAuth":{"enabled":false,"clientSecret":"******"},"facebookAuth":{"enabled":false,"clientSecret":"******"},"githubAuth":{"enabled":false,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"clientSecret":"******"},"discordAuth":{"enabled":false,"clientSecret":"******"},"twitterAuth":{"enabled":false,"clientSecret":"******"},"microsoftAuth":{"enabled":false,"clientSecret":"******"},"spotifyAuth":{"enabled":false,"clientSecret":"******"},"kakaoAuth":{"enabled":false,"clientSecret":"******"},"twitchAuth":{"enabled":false,"clientSecret":"******"},"stravaAuth":{"enabled":false,"clientSecret":"******"}}`

if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected\n%v\ngot\n%v", expected, encodedStr)
Expand All @@ -227,6 +233,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.SpotifyAuth.ClientId = "spotify_test"
s.KakaoAuth.ClientId = "kakao_test"
s.TwitchAuth.ClientId = "twitch_test"
s.StravaAuth.ClientId = "strava_test"

result := s.NamedAuthProviderConfigs()

Expand All @@ -247,6 +254,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
`"twitter":{"enabled":false,"clientId":"twitter_test"}`,
`"kakao":{"enabled":false,"clientId":"kakao_test"}`,
`"twitch":{"enabled":false,"clientId":"twitch_test"}`,
`"strava":{"enabled":false,"clientId":"strava_test"}`,
}
for _, p := range expectedParts {
if !strings.Contains(encodedStr, p) {
Expand Down
2 changes: 2 additions & 0 deletions tools/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ func NewProviderByName(name string) (Provider, error) {
return NewKakaoProvider(), nil
case NameTwitch:
return NewTwitchProvider(), nil
case NameStrava:
return NewStravaProvider(), nil
default:
return nil, errors.New("Missing provider " + name)
}
Expand Down
9 changes: 9 additions & 0 deletions tools/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,13 @@ func TestNewProviderByName(t *testing.T) {
if _, ok := p.(*auth.Twitch); !ok {
t.Error("Expected to be instance of *auth.Twitch")
}

// strava
p, err = auth.NewProviderByName(auth.NameStrava)
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.Strava); !ok {
t.Error("Expected to be instance of *auth.Strava")
}
}
70 changes: 70 additions & 0 deletions tools/auth/strava.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package auth

import (
"encoding/json"
"strconv"

"golang.org/x/oauth2"
)

var _ Provider = (*Strava)(nil)

// NameStrava is the unique name of the Strava provider.
const NameStrava string = "strava"

// Strava allows authentication via Strava OAuth2.
type Strava struct {
*baseProvider
}

// NewStravaProvider creates new Strava provider instance with some defaults.
func NewStravaProvider() *Strava {
return &Strava{&baseProvider{
scopes: []string{
"profile:read_all",
},
authUrl: "https://www.strava.com/oauth/authorize",
tokenUrl: "https://www.strava.com/api/v3/oauth/token",
userApiUrl: "https://www.strava.com/api/v3/athlete",
}}
}

// FetchAuthUser returns an AuthUser instance based on the Strava's user api.
//
// API reference: https://developers.strava.com/docs/authentication/
func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserData(token)
if err != nil {
return nil, err
}

rawUser := map[string]any{}
if err := json.Unmarshal(data, &rawUser); err != nil {
return nil, err
}

extracted := struct {
Id int `json:"id"`
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
Username string `json:"username"`
ProfileImageUrl string `json:"profile"`

// At the time of writing, Strava OAuth2 doesn't support returning the user email address
// Email string `json:"email"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}

user := &AuthUser{
Id: strconv.Itoa(extracted.Id),
Name: extracted.FirstName + " " + extracted.LastName,
Username: extracted.Username,
AvatarUrl: extracted.ProfileImageUrl,
RawUser: rawUser,
AccessToken: token.AccessToken,
}

return user, nil
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import{S as ke,i as be,s as ge,e as r,w as b,b as g,c as _e,f as k,g as h,h as n,m as me,x as G,O as re,P as we,k as ve,Q as Ce,n as Pe,t as L,a as Y,o as _,d as pe,R as Me,C as Se,p as $e,r as H,u as je,N as Ae}from"./index.39461150.js";import{S as Be}from"./SdkTabs.6c109619.js";function ue(a,l,o){const s=a.slice();return s[5]=l[o],s}function de(a,l,o){const s=a.slice();return s[5]=l[o],s}function fe(a,l){let o,s=l[5].code+"",m,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),m=b(s),f=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(v,C){h(v,o,C),n(o,m),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(m,s),C&6&&H(o,"active",l[1]===l[5].code)},d(v){v&&_(o),i=!1,u()}}}function he(a,l){let o,s,m,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),_e(s.$$.fragment),m=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),me(s,o,null),n(o,m),f=!0},p(i,u){l=i;const d={};u&4&&(d.content=l[5].body),s.$set(d),(!f||u&6)&&H(o,"active",l[1]===l[5].code)},i(i){f||(L(s.$$.fragment,i),f=!0)},o(i){Y(s.$$.fragment,i),f=!1},d(i){i&&_(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",m,f,i,u,d,v,C,F=a[0].name+"",U,R,q,P,D,j,W,M,K,X,Q,A,Z,V,y=a[0].name+"",I,x,E,B,J,S,O,w=[],ee=new Map,te,T,p=[],le=new Map,$;P=new Be({props:{js:`
import{S as ke,i as be,s as ge,e as r,w as b,b as g,c as _e,f as k,g as h,h as n,m as me,x as G,O as re,P as we,k as ve,Q as Ce,n as Pe,t as L,a as Y,o as _,d as pe,R as Me,C as Se,p as $e,r as H,u as je,N as Ae}from"./index.b8a14a87.js";import{S as Be}from"./SdkTabs.df0d020f.js";function ue(a,l,o){const s=a.slice();return s[5]=l[o],s}function de(a,l,o){const s=a.slice();return s[5]=l[o],s}function fe(a,l){let o,s=l[5].code+"",m,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),m=b(s),f=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(v,C){h(v,o,C),n(o,m),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(m,s),C&6&&H(o,"active",l[1]===l[5].code)},d(v){v&&_(o),i=!1,u()}}}function he(a,l){let o,s,m,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),_e(s.$$.fragment),m=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),me(s,o,null),n(o,m),f=!0},p(i,u){l=i;const d={};u&4&&(d.content=l[5].body),s.$set(d),(!f||u&6)&&H(o,"active",l[1]===l[5].code)},i(i){f||(L(s.$$.fragment,i),f=!0)},o(i){Y(s.$$.fragment,i),f=!1},d(i){i&&_(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",m,f,i,u,d,v,C,F=a[0].name+"",U,R,q,P,D,j,W,M,K,X,Q,A,Z,V,y=a[0].name+"",I,x,E,B,J,S,O,w=[],ee=new Map,te,T,p=[],le=new Map,$;P=new Be({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[3]}');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as c,h as o,m as ne,x as re,O as qe,P as xe,k as Ie,Q as Je,n as Ke,t as U,a as j,o as d,d as ie,R as Qe,C as He,p as We,r as x,u as Ge}from"./index.39461150.js";import{S as Xe}from"./SdkTabs.6c109619.js";function Ee(r,l,s){const n=r.slice();return n[5]=l[s],n}function Fe(r,l,s){const n=r.slice();return n[5]=l[s],n}function Le(r,l){let s,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){s=a("button"),m=k(n),_=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(g,w){c(g,s,w),o(s,m),o(s,_),i||(f=Ge(s,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&x(s,"active",l[1]===l[5].code)},d(g){g&&d(s),i=!1,f()}}}function Ne(r,l){let s,n,m,_;return n=new Ve({props:{content:l[5].body}}),{key:r,first:null,c(){s=a("div"),ae(n.$$.fragment),m=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(i,f){c(i,s,f),ne(n,s,null),o(s,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&x(s,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&d(s),ie(n)}}}function Ye(r){var Be,Me;let l,s,n=r[0].name+"",m,_,i,f,v,g,w,B,I,S,F,ce,L,M,de,J,N=r[0].name+"",K,ue,pe,V,Q,D,W,T,G,fe,X,C,Y,he,Z,be,h,me,P,_e,ke,ve,ee,ge,te,ye,Se,$e,oe,we,le,O,se,R,q,$=[],Te=new Map,Ce,H,y=[],Re=new Map,A;g=new Xe({props:{js:`
import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as c,h as o,m as ne,x as re,O as qe,P as xe,k as Ie,Q as Je,n as Ke,t as U,a as j,o as d,d as ie,R as Qe,C as He,p as We,r as x,u as Ge}from"./index.b8a14a87.js";import{S as Xe}from"./SdkTabs.df0d020f.js";function Ee(r,l,s){const n=r.slice();return n[5]=l[s],n}function Fe(r,l,s){const n=r.slice();return n[5]=l[s],n}function Le(r,l){let s,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){s=a("button"),m=k(n),_=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(g,w){c(g,s,w),o(s,m),o(s,_),i||(f=Ge(s,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&x(s,"active",l[1]===l[5].code)},d(g){g&&d(s),i=!1,f()}}}function Ne(r,l){let s,n,m,_;return n=new Ve({props:{content:l[5].body}}),{key:r,first:null,c(){s=a("div"),ae(n.$$.fragment),m=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(i,f){c(i,s,f),ne(n,s,null),o(s,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&x(s,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&d(s),ie(n)}}}function Ye(r){var Be,Me;let l,s,n=r[0].name+"",m,_,i,f,v,g,w,B,I,S,F,ce,L,M,de,J,N=r[0].name+"",K,ue,pe,V,Q,D,W,T,G,fe,X,C,Y,he,Z,be,h,me,P,_e,ke,ve,ee,ge,te,ye,Se,$e,oe,we,le,O,se,R,q,$=[],Te=new Map,Ce,H,y=[],Re=new Map,A;g=new Xe({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${r[3]}');
Expand Down
Loading

0 comments on commit d710446

Please sign in to comment.