Skip to content

Commit

Permalink
[pocketbase#887] added Spotify OAuth2 provider
Browse files Browse the repository at this point in the history
  • Loading branch information
ollema authored Nov 1, 2022
1 parent 9cef6eb commit 639522b
Show file tree
Hide file tree
Showing 35 changed files with 121 additions and 35 deletions.
3 changes: 3 additions & 0 deletions apis/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func TestSettingsList(t *testing.T) {
`"twitterAuth":{`,
`"discordAuth":{`,
`"microsoftAuth":{`,
`"spotifyAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
},
Expand Down Expand Up @@ -119,6 +120,7 @@ func TestSettingsSet(t *testing.T) {
`"gitlabAuth":{`,
`"discordAuth":{`,
`"microsoftAuth":{`,
`"spotifyAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"Acme"`,
Expand Down Expand Up @@ -172,6 +174,7 @@ func TestSettingsSet(t *testing.T) {
`"twitterAuth":{`,
`"discordAuth":{`,
`"microsoftAuth":{`,
`"spotifyAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"update_test"`,
Expand Down
7 changes: 7 additions & 0 deletions core/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Settings struct {
DiscordAuth AuthProviderConfig `form:"discordAuth" json:"discordAuth"`
TwitterAuth AuthProviderConfig `form:"twitterAuth" json:"twitterAuth"`
MicrosoftAuth AuthProviderConfig `form:"microsoftAuth" json:"microsoftAuth"`
SpotifyAuth AuthProviderConfig `form:"spotifyAuth" json:"spotifyAuth"`
}

// NewSettings creates and returns a new default Settings instance.
Expand Down Expand Up @@ -111,6 +112,9 @@ func NewSettings() *Settings {
MicrosoftAuth: AuthProviderConfig{
Enabled: false,
},
SpotifyAuth: AuthProviderConfig{
Enabled: false,
},
}
}

Expand All @@ -137,6 +141,7 @@ func (s *Settings) Validate() error {
validation.Field(&s.DiscordAuth),
validation.Field(&s.TwitterAuth),
validation.Field(&s.MicrosoftAuth),
validation.Field(&s.SpotifyAuth),
)
}

Expand Down Expand Up @@ -188,6 +193,7 @@ func (s *Settings) RedactClone() (*Settings, error) {
&clone.DiscordAuth.ClientSecret,
&clone.TwitterAuth.ClientSecret,
&clone.MicrosoftAuth.ClientSecret,
&clone.SpotifyAuth.ClientSecret,
}

// mask all sensitive fields
Expand All @@ -214,6 +220,7 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
auth.NameDiscord: s.DiscordAuth,
auth.NameTwitter: s.TwitterAuth,
auth.NameMicrosoft: s.MicrosoftAuth,
auth.NameSpotify: s.SpotifyAuth,
}
}

Expand Down
11 changes: 9 additions & 2 deletions core/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func TestSettingsValidate(t *testing.T) {
s.TwitterAuth.ClientId = ""
s.MicrosoftAuth.Enabled = true
s.MicrosoftAuth.ClientId = ""
s.SpotifyAuth.Enabled = true
s.SpotifyAuth.ClientId = ""

// check if Validate() is triggering the members validate methods.
err := s.Validate()
Expand All @@ -66,6 +68,7 @@ func TestSettingsValidate(t *testing.T) {
`"discordAuth":{`,
`"twitterAuth":{`,
`"microsoftAuth":{`,
`"spotifyAuth":{`,
}

errBytes, _ := json.Marshal(err)
Expand Down Expand Up @@ -108,6 +111,8 @@ func TestSettingsMerge(t *testing.T) {
s2.TwitterAuth.ClientId = "twitter_test"
s2.MicrosoftAuth.Enabled = true
s2.MicrosoftAuth.ClientId = "microsoft_test"
s2.SpotifyAuth.Enabled = true
s2.SpotifyAuth.ClientId = "spotify_test"

if err := s1.Merge(s2); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -176,6 +181,7 @@ func TestSettingsRedactClone(t *testing.T) {
s1.DiscordAuth.ClientSecret = "test123"
s1.TwitterAuth.ClientSecret = "test123"
s1.MicrosoftAuth.ClientSecret = "test123"
s1.SpotifyAuth.ClientSecret = "test123"

s2, err := s1.RedactClone()
if err != nil {
Expand All @@ -187,7 +193,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":"******","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":"******"}}`
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":"******","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":"******"}}`

if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected\n%v\ngot\n%v", expected, encodedStr)
Expand All @@ -205,6 +211,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.DiscordAuth.ClientId = "discord_test"
s.TwitterAuth.ClientId = "twitter_test"
s.MicrosoftAuth.ClientId = "microsoft_test"
s.SpotifyAuth.ClientId = "spotify_test"

result := s.NamedAuthProviderConfigs()

Expand All @@ -213,7 +220,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
t.Fatal(err)
}

expected := `{"discord":{"enabled":false,"clientId":"discord_test"},"facebook":{"enabled":false,"clientId":"facebook_test"},"github":{"enabled":false,"clientId":"github_test"},"gitlab":{"enabled":true,"clientId":"gitlab_test"},"google":{"enabled":false,"clientId":"google_test"},"microsoft":{"enabled":false,"clientId":"microsoft_test"},"twitter":{"enabled":false,"clientId":"twitter_test"}}`
expected := `{"discord":{"enabled":false,"clientId":"discord_test"},"facebook":{"enabled":false,"clientId":"facebook_test"},"github":{"enabled":false,"clientId":"github_test"},"gitlab":{"enabled":true,"clientId":"gitlab_test"},"google":{"enabled":false,"clientId":"google_test"},"microsoft":{"enabled":false,"clientId":"microsoft_test"},"spotify":{"enabled":false,"clientId":"spotify_test"},"twitter":{"enabled":false,"clientId":"twitter_test"}}`

if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected the same serialization, got \n%v", encodedStr)
Expand Down
2 changes: 2 additions & 0 deletions tools/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ func NewProviderByName(name string) (Provider, error) {
return NewTwitterProvider(), nil
case NameMicrosoft:
return NewMicrosoftProvider(), nil
case NameSpotify:
return NewSpotifyProvider(), 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 @@ -72,4 +72,13 @@ func TestNewProviderByName(t *testing.T) {
if _, ok := p.(*auth.Microsoft); !ok {
t.Error("Expected to be instance of *auth.Microsoft")
}

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

import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/spotify"
)

var _ Provider = (*Spotify)(nil)

// NameSpotify is the unique name of the Spotify provider.
const NameSpotify string = "spotify"

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

// NewSpotifyProvider creates a new Spotify provider instance with some defaults.
func NewSpotifyProvider() *Spotify {
return &Spotify{&baseProvider{
scopes: []string{"user-read-private", "user-read-email"},
authUrl: spotify.Endpoint.AuthURL,
tokenUrl: spotify.Endpoint.TokenURL,
userApiUrl: "https://api.spotify.com/v1/me",
}}
}

// FetchAuthUser returns an AuthUser instance based on the Spotify's user api.
func (p *Spotify) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile
rawData := struct {
Id string `json:"id"`
Name string `json:"display_name"`
Email string `json:"email"`
Images []struct {
Url string `json:"url"`
} `json:"images"`
}{}

if err := p.FetchRawUserData(token, &rawData); err != nil {
return nil, err
}

user := &AuthUser{
Id: rawData.Id,
Name: rawData.Name,
Email: rawData.Email,
}
if len(rawData.Images) > 0 {
user.AvatarUrl = rawData.Images[0].Url
}

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,P as re,Q as we,k as ve,R as Ce,n as Pe,t as J,a as Y,o as _,d as pe,L as Me,C as Se,p as $e,r as H,u as je,O as Ae}from"./index.7b2502cb.js";import{S as Be}from"./SdkTabs.315f7f19.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||(J(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,K=a[0].name+"",U,R,q,P,D,j,W,M,N,X,Q,A,Z,V,y=a[0].name+"",I,x,L,B,E,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,P as re,Q as we,k as ve,R as Ce,n as Pe,t as J,a as Y,o as _,d as pe,L as Me,C as Se,p as $e,r as H,u as je,O as Ae}from"./index.be8ffbe5.js";import{S as Be}from"./SdkTabs.8f55857f.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||(J(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,K=a[0].name+"",U,R,q,P,D,j,W,M,N,X,Q,A,Z,V,y=a[0].name+"",I,x,L,B,E,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 Ne,i as Ue,s as je,O as ze,e as s,w as k,b as p,c as se,f as b,g as c,h as o,m as ne,x as re,P as Oe,Q as Ie,k as Je,R as Ke,n as Qe,t as U,a as j,o as d,d as ie,L as xe,C as Fe,p as We,r as I,u as Ge}from"./index.7b2502cb.js";import{S as Xe}from"./SdkTabs.315f7f19.js";function He(r,l,a){const n=r.slice();return n[5]=l[a],n}function Le(r,l,a){const n=r.slice();return n[5]=l[a],n}function Ee(r,l){let a,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){a=s("button"),m=k(n),_=p(),b(a,"class","tab-item"),I(a,"active",l[1]===l[5].code),this.first=a},m(g,w){c(g,a,w),o(a,m),o(a,_),i||(f=Ge(a,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&I(a,"active",l[1]===l[5].code)},d(g){g&&d(a),i=!1,f()}}}function Ve(r,l){let a,n,m,_;return n=new ze({props:{content:l[5].body}}),{key:r,first:null,c(){a=s("div"),se(n.$$.fragment),m=p(),b(a,"class","tab-item"),I(a,"active",l[1]===l[5].code),this.first=a},m(i,f){c(i,a,f),ne(n,a,null),o(a,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&I(a,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&d(a),ie(n)}}}function Ye(r){var Be,Me;let l,a,n=r[0].name+"",m,_,i,f,v,g,w,B,J,S,L,ce,E,M,de,K,V=r[0].name+"",Q,ue,pe,z,x,q,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,D,ae,R,O,$=[],Te=new Map,Ce,F,y=[],Re=new Map,A;g=new Xe({props:{js:`
import{S as Ne,i as Ue,s as je,O as ze,e as s,w as k,b as p,c as se,f as b,g as c,h as o,m as ne,x as re,P as Oe,Q as Ie,k as Je,R as Ke,n as Qe,t as U,a as j,o as d,d as ie,L as xe,C as Fe,p as We,r as I,u as Ge}from"./index.be8ffbe5.js";import{S as Xe}from"./SdkTabs.8f55857f.js";function He(r,l,a){const n=r.slice();return n[5]=l[a],n}function Le(r,l,a){const n=r.slice();return n[5]=l[a],n}function Ee(r,l){let a,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){a=s("button"),m=k(n),_=p(),b(a,"class","tab-item"),I(a,"active",l[1]===l[5].code),this.first=a},m(g,w){c(g,a,w),o(a,m),o(a,_),i||(f=Ge(a,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&I(a,"active",l[1]===l[5].code)},d(g){g&&d(a),i=!1,f()}}}function Ve(r,l){let a,n,m,_;return n=new ze({props:{content:l[5].body}}),{key:r,first:null,c(){a=s("div"),se(n.$$.fragment),m=p(),b(a,"class","tab-item"),I(a,"active",l[1]===l[5].code),this.first=a},m(i,f){c(i,a,f),ne(n,a,null),o(a,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&I(a,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&d(a),ie(n)}}}function Ye(r){var Be,Me;let l,a,n=r[0].name+"",m,_,i,f,v,g,w,B,J,S,L,ce,E,M,de,K,V=r[0].name+"",Q,ue,pe,z,x,q,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,D,ae,R,O,$=[],Te=new Map,Ce,F,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 639522b

Please sign in to comment.