Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apis/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func TestSettingsList(t *testing.T) {
`"patreonAuth":{`,
`"mailcowAuth":{`,
`"bitbucketAuth":{`,
`"donationalertsAuth":{`,
`"planningcenterAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
Expand Down Expand Up @@ -172,6 +173,7 @@ func TestSettingsSet(t *testing.T) {
`"patreonAuth":{`,
`"mailcowAuth":{`,
`"bitbucketAuth":{`,
`"donationalertsAuth":{`,
`"planningcenterAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
Expand Down Expand Up @@ -246,6 +248,7 @@ func TestSettingsSet(t *testing.T) {
`"patreonAuth":{`,
`"mailcowAuth":{`,
`"bitbucketAuth":{`,
`"donationalertsAuth":{`,
`"planningcenterAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
Expand Down
7 changes: 7 additions & 0 deletions models/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Settings struct {
MailcowAuth AuthProviderConfig `form:"mailcowAuth" json:"mailcowAuth"`
BitbucketAuth AuthProviderConfig `form:"bitbucketAuth" json:"bitbucketAuth"`
PlanningcenterAuth AuthProviderConfig `form:"planningcenterAuth" json:"planningcenterAuth"`
DonationAlertsAuth AuthProviderConfig `form:"donationalertsAuth" json:"donationalertsAuth"`
}

// New creates and returns a new default Settings instance.
Expand Down Expand Up @@ -204,6 +205,9 @@ func New() *Settings {
PlanningcenterAuth: AuthProviderConfig{
Enabled: false,
},
DonationAlertsAuth: AuthProviderConfig{
Enabled: false,
},
}
}

Expand Down Expand Up @@ -251,6 +255,7 @@ func (s *Settings) Validate() error {
validation.Field(&s.MailcowAuth),
validation.Field(&s.BitbucketAuth),
validation.Field(&s.PlanningcenterAuth),
validation.Field(&s.DonationAlertsAuth),
)
}

Expand Down Expand Up @@ -321,6 +326,7 @@ func (s *Settings) RedactClone() (*Settings, error) {
&clone.MailcowAuth.ClientSecret,
&clone.BitbucketAuth.ClientSecret,
&clone.PlanningcenterAuth.ClientSecret,
&clone.DonationAlertsAuth.ClientSecret,
}

// mask all sensitive fields
Expand Down Expand Up @@ -365,6 +371,7 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
auth.NameMailcow: s.MailcowAuth,
auth.NameBitbucket: s.BitbucketAuth,
auth.NamePlanningcenter: s.PlanningcenterAuth,
auth.NameDonationAlerts: s.DonationAlertsAuth,
}
}

Expand Down
8 changes: 8 additions & 0 deletions models/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ func TestSettingsValidate(t *testing.T) {
s.BitbucketAuth.ClientId = ""
s.PlanningcenterAuth.Enabled = true
s.PlanningcenterAuth.ClientId = ""
s.DonationAlertsAuth.Enabled = true
s.DonationAlertsAuth.ClientId = ""

// check if Validate() is triggering the members validate methods.
err := s.Validate()
Expand Down Expand Up @@ -127,6 +129,7 @@ func TestSettingsValidate(t *testing.T) {
`"mailcowAuth":{`,
`"bitbucketAuth":{`,
`"planningcenterAuth":{`,
`"donationAlertsAuth":{`,
}

errBytes, _ := json.Marshal(err)
Expand Down Expand Up @@ -208,6 +211,8 @@ func TestSettingsMerge(t *testing.T) {
s2.BitbucketAuth.ClientId = "bitbucket_test"
s2.PlanningcenterAuth.Enabled = true
s2.PlanningcenterAuth.ClientId = "planningcenter_test"
s2.DonationAlertsAuth.Enabled = true
s2.DonationAlertsAuth.ClientId = "donationalerts_test"

if err := s1.Merge(s2); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -302,6 +307,7 @@ func TestSettingsRedactClone(t *testing.T) {
s1.MailcowAuth.ClientSecret = testSecret
s1.BitbucketAuth.ClientSecret = testSecret
s1.PlanningcenterAuth.ClientSecret = testSecret
s1.DonationAlertsAuth.ClientSecret = testSecret

s1Bytes, err := json.Marshal(s1)
if err != nil {
Expand Down Expand Up @@ -364,6 +370,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.MailcowAuth.ClientId = "mailcow_test"
s.BitbucketAuth.ClientId = "bitbucket_test"
s.PlanningcenterAuth.ClientId = "planningcenter_test"
s.DonationAlertsAuth.ClientId = "donationalerts_test"

result := s.NamedAuthProviderConfigs()

Expand Down Expand Up @@ -399,6 +406,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
`"mailcow":{"enabled":false,"clientId":"mailcow_test"`,
`"bitbucket":{"enabled":false,"clientId":"bitbucket_test"`,
`"planningcenter":{"enabled":false,"clientId":"planningcenter_test"`,
`"donationalerts":{"enabled":false,"clientId":"donationalerts_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 @@ -158,6 +158,8 @@ func NewProviderByName(name string) (Provider, error) {
return NewBitbucketProvider(), nil
case NamePlanningcenter:
return NewPlanningcenterProvider(), nil
case NameDonationAlerts:
return NewDonationAlertsProvider(), 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 @@ -243,4 +243,13 @@ func TestNewProviderByName(t *testing.T) {
if _, ok := p.(*auth.Planningcenter); !ok {
t.Error("Expected to be instance of *auth.Planningcenter")
}

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

import (
"context"
"encoding/json"
"strconv"

"github.com/pocketbase/pocketbase/tools/types"
"golang.org/x/oauth2"
)

var _ Provider = (*DonationAlerts)(nil)

// NameDonationAlerts is the unique name of the Donation Alerts provider.
const NameDonationAlerts string = "donationalerts"

// Donation Alerts allows authentication via Discord OAuth2.
type DonationAlerts struct {
*baseProvider
}

// NewDiscordProvider creates a new Discord provider instance with some defaults.
func NewDonationAlertsProvider() *DonationAlerts {
// https://www.donationalerts.com/apidoc#authorization
return &DonationAlerts{&baseProvider{
ctx: context.Background(),
displayName: "Donation Alerts",
pkce: true,
scopes: []string{"oauth-user-show"},
authUrl: "https://www.donationalerts.com/oauth/authorize",
tokenUrl: "https://www.donationalerts.com/oauth/token",
userApiUrl: "https://www.donationalerts.com/api/v1/user/oauth",
}}
}

// FetchAuthUser returns an AuthUser instance from Discord's user api.
//
// API reference: https://discord.com/developers/docs/resources/user#user-object
func (p *DonationAlerts) 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 {
Data struct {
Id int `json:"id"`
UniqueName string `json:"code"`
Name string `json:"name"`
Avatar string `json:"avatar"`
Email string `json:"email"`
} `json:"data"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}

user := &AuthUser{
Id: strconv.Itoa(extracted.Data.Id),
Name: extracted.Data.UniqueName,
Username: extracted.Data.Name,
Email: extracted.Data.Email,
AvatarUrl: extracted.Data.Avatar,
RawUser: rawUser,
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
}

user.Expiry, _ = types.ParseDateTime(token.Expiry)

return user, nil
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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

const pb = new PocketBase('${r[3]}');
Expand Down Expand Up @@ -26,12 +26,12 @@ import{S as je,i as xe,s as Je,N as Ue,O as J,e as s,v as k,b as p,c as K,f as b
print(pb.authStore.model.id);
`}}),R=new Ue({props:{content:"?expand=relField1,relField2.subRelField"}}),A=new et({props:{prefix:"record."}});let x=J(r[2]);const Be=e=>e[5].code;for(let e=0;e<x.length;e+=1){let t=Ve(r,x,e),c=Be(t);Re.set(c,y[e]=ze(c,t))}let L=J(r[2]);const Me=e=>e[5].code;for(let e=0;e<L.length;e+=1){let t=Ne(r,L,e),c=Me(t);qe.set(c,$[e]=Qe(c,t))}return{c(){l=s("h3"),a=k("Auth refresh ("),m=k(n),_=k(")"),i=p(),h=s("div"),h.innerHTML=`<p>Returns a new auth response (token and record data) for an
<strong>already authenticated record</strong>.</p> <p><em>This method is usually called by users on page/screen reload to ensure that the previously stored
data in <code>pb.authStore</code> is still valid and up-to-date.</em></p>`,g=p(),K(v.$$.fragment),w=p(),B=s("h6"),B.textContent="API details",X=p(),S=s("div"),z=s("strong"),z.textContent="POST",ue=p(),Q=s("div"),M=s("p"),pe=k("/api/collections/"),Y=s("strong"),Z=k(U),he=k("/auth-refresh"),fe=p(),j=s("p"),j.innerHTML="Requires record <code>Authorization:TOKEN</code> header",ee=p(),D=s("div"),D.textContent="Query parameters",te=p(),T=s("table"),oe=s("thead"),oe.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',be=p(),F=s("tbody"),C=s("tr"),le=s("td"),le.textContent="expand",me=p(),ae=s("td"),ae.innerHTML='<span class="label">String</span>',_e=p(),f=s("td"),ke=k(`Auto expand record relations. Ex.:
`),K(R.$$.fragment),ge=k(`
Supports up to 6-levels depth nested relations expansion. `),ve=s("br"),$e=k(`
The expanded relations will be appended to the record under the
`),se=s("code"),se.textContent="expand",ye=k(" property (eg. "),ne=s("code"),ne.textContent='"expand": {"relField1": {...}, ...}',Se=k(`).
`),we=s("br"),Te=k(`
data in <code>pb.authStore</code> is still valid and up-to-date.</em></p>`,g=p(),K(v.$$.fragment),w=p(),B=s("h6"),B.textContent="API details",X=p(),S=s("div"),z=s("strong"),z.textContent="POST",ue=p(),Q=s("div"),M=s("p"),pe=k("/api/collections/"),Y=s("strong"),Z=k(U),he=k("/auth-refresh"),fe=p(),j=s("p"),j.innerHTML="Requires record <code>Authorization:TOKEN</code> header",ee=p(),D=s("div"),D.textContent="Query parameters",te=p(),T=s("table"),oe=s("thead"),oe.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',be=p(),F=s("tbody"),C=s("tr"),le=s("td"),le.textContent="expand",me=p(),ae=s("td"),ae.innerHTML='<span class="label">String</span>',_e=p(),f=s("td"),ke=k(`Auto expand record relations. Ex.:\r
`),K(R.$$.fragment),ge=k(`\r
Supports up to 6-levels depth nested relations expansion. `),ve=s("br"),$e=k(`\r
The expanded relations will be appended to the record under the\r
`),se=s("code"),se.textContent="expand",ye=k(" property (eg. "),ne=s("code"),ne.textContent='"expand": {"relField1": {...}, ...}',Se=k(`).\r
`),we=s("br"),Te=k(`\r
Only the relations to which the request user has permissions to `),re=s("strong"),re.textContent="view",Ce=k(" will be expanded."),Pe=p(),K(A.$$.fragment),ie=p(),O=s("div"),O.textContent="Responses",ce=p(),P=s("div"),H=s("div");for(let e=0;e<y.length;e+=1)y[e].c();Ae=p(),E=s("div");for(let e=0;e<$.length;e+=1)$[e].c();b(l,"class","m-b-sm"),b(h,"class","content txt-lg m-b-sm"),b(B,"class","m-b-xs"),b(z,"class","label label-primary"),b(Q,"class","content"),b(j,"class","txt-hint txt-sm txt-right"),b(S,"class","alert alert-success"),b(D,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(O,"class","section-title"),b(H,"class","tabs-header compact combined left"),b(E,"class","tabs-content"),b(P,"class","tabs")},m(e,t){d(e,l,t),o(l,a),o(l,m),o(l,_),d(e,i,t),d(e,h,t),d(e,g,t),I(v,e,t),d(e,w,t),d(e,B,t),d(e,X,t),d(e,S,t),o(S,z),o(S,ue),o(S,Q),o(Q,M),o(M,pe),o(M,Y),o(Y,Z),o(M,he),o(S,fe),o(S,j),d(e,ee,t),d(e,D,t),d(e,te,t),d(e,T,t),o(T,oe),o(T,be),o(T,F),o(F,C),o(C,le),o(C,me),o(C,ae),o(C,_e),o(C,f),o(f,ke),I(R,f,null),o(f,ge),o(f,ve),o(f,$e),o(f,se),o(f,ye),o(f,ne),o(f,Se),o(f,we),o(f,Te),o(f,re),o(f,Ce),o(F,Pe),I(A,F,null),d(e,ie,t),d(e,O,t),d(e,ce,t),d(e,P,t),o(P,H);for(let c=0;c<y.length;c+=1)y[c]&&y[c].m(H,null);o(P,Ae),o(P,E);for(let c=0;c<$.length;c+=1)$[c]&&$[c].m(E,null);q=!0},p(e,[t]){var Oe,He;(!q||t&1)&&n!==(n=e[0].name+"")&&de(m,n);const c={};t&9&&(c.js=`
import PocketBase from 'pocketbase';

Expand Down
Loading