@proveanything/smartlinks 2.0.69 → 2.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/asset.d.ts +5 -0
- package/dist/api/asset.js +11 -0
- package/dist/api/authKit.d.ts +28 -4
- package/dist/api/authKit.js +35 -1
- package/dist/api/bookings.d.ts +89 -0
- package/dist/api/bookings.js +179 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/docs/API_SUMMARY.md +503 -11
- package/dist/docs/app-objects.md +4 -0
- package/dist/docs/assets.md +63 -2
- package/dist/docs/auth-kit.md +46 -5
- package/dist/docs/bookings.md +324 -0
- package/dist/docs/members.md +182 -0
- package/dist/docs/overview.md +477 -475
- package/dist/docs/server-functions.md +4 -1
- package/dist/http.js +3 -0
- package/dist/openapi.yaml +5254 -4037
- package/dist/testing/index.js +1 -1
- package/dist/types/appManifest.d.ts +16 -4
- package/dist/types/asset.d.ts +25 -1
- package/dist/types/authKit.d.ts +58 -7
- package/dist/types/bookings.d.ts +301 -0
- package/dist/types/bookings.js +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/freeTimes.d.ts +57 -0
- package/dist/utils/freeTimes.js +206 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/API_SUMMARY.md +503 -11
- package/docs/app-objects.md +4 -0
- package/docs/assets.md +63 -2
- package/docs/auth-kit.md +46 -5
- package/docs/bookings.md +324 -0
- package/docs/members.md +182 -0
- package/docs/overview.md +477 -475
- package/docs/server-functions.md +4 -1
- package/openapi.yaml +5254 -4037
- package/package.json +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Members: signed-in people with a level
|
|
2
|
+
|
|
3
|
+
A club, a league, a wine club, a members' area. SmartLinks has **no separate "membership" object**,
|
|
4
|
+
and you don't need one:
|
|
5
|
+
|
|
6
|
+
| Piece | Where it lives |
|
|
7
|
+
|---|---|
|
|
8
|
+
| A member's login | The **auth kit**: the site's own logins (auth-kit.md → "Own logins") |
|
|
9
|
+
| The member | Their **contact** (one per person, created when they sign in or fill in a form) |
|
|
10
|
+
| What they are (gold, silver, junior, pending) | A **contact custom field**, e.g. `customFields.level` |
|
|
11
|
+
| Who may see or change it | The contact field's **public** setting: hidden, visible or editable |
|
|
12
|
+
| Members-only content | A **server function** that checks the level, then returns the content |
|
|
13
|
+
| Managing members | **Studio → Customers**: find people, set their level, email a segment |
|
|
14
|
+
|
|
15
|
+
**The contract: whoever manages membership writes the member's `level` field.** That can be the owner
|
|
16
|
+
by hand in Studio, a memberships app after a payment, or a connector syncing a club's existing system.
|
|
17
|
+
The site only ever reads the field, so the way people become members can change without rebuilding
|
|
18
|
+
the site.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Questions to settle first
|
|
23
|
+
|
|
24
|
+
Ask these before building, because they change what you build:
|
|
25
|
+
|
|
26
|
+
1. **Who can join?** Anyone; anyone, but the club confirms them; or **only people the club invites**
|
|
27
|
+
(no sign-up at all)?
|
|
28
|
+
2. **Who sets the level?** The owner (people sign up, then the owner confirms them), or the person
|
|
29
|
+
themselves? Let people choose only a level that unlocks nothing, like "junior / adult".
|
|
30
|
+
3. **What does each level get?** Pages, results, discounts, early access, booking.
|
|
31
|
+
4. **Fees?** If the club takes payment by hand, the owner sets the level once they've been paid. Paying
|
|
32
|
+
on the site is a memberships app's job. Never take card details in a site.
|
|
33
|
+
5. **Do members see each other?** A members' directory needs consent: a "show me in the directory"
|
|
34
|
+
field the member sets themselves.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 1. Sign-in: the site's own logins
|
|
39
|
+
|
|
40
|
+
- A site's members sign in through its auth kit. The collection's kit is `collection.defaultAuthKitId`
|
|
41
|
+
(`SL.collection.get(collectionId)`).
|
|
42
|
+
- A club should have **its own logins**, so its members are accounts of this site only. In Forge: the
|
|
43
|
+
Site tab → Members → "Set up member sign-in" (or "Use my own members" on a site that has the shared
|
|
44
|
+
login). That creates the kit if needed, makes it the site's, and switches it to its own members.
|
|
45
|
+
- Use the flows in auth-kit.md: register, login, magic link, Google, password reset, email verification.
|
|
46
|
+
Never build a login of your own, and never store passwords.
|
|
47
|
+
- On sign-in, the SDK keeps the member's session (`setBearerToken`). Persist `session.token`
|
|
48
|
+
(respect `rememberMe`), restore it on load with `SL.setBearerToken(token)`, and clear it on sign-out.
|
|
49
|
+
|
|
50
|
+
By default anyone can create a login, and **being a member is the level, not the login**: an approval
|
|
51
|
+
club is just "signed in, no level yet".
|
|
52
|
+
|
|
53
|
+
**Invitation only.** A site with its own logins can turn sign-up off: `SL.authKit.update(collectionId,
|
|
54
|
+
authKitId, { signUp: 'invite' })` (or the owner, in the Customer Login app: Members → Who can join). Then nobody can
|
|
55
|
+
create an account by any route: password, magic link, phone, Google and the rest refuse with
|
|
56
|
+
`SIGN_UP_CLOSED`, and "email me a link" answers the same for strangers without sending anything. The
|
|
57
|
+
site's admins invite people instead:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// admin session (the site's admin page, Studio): their account, their contact with a level, and an email
|
|
61
|
+
await SL.authKit.invite(collectionId, authKitId, {
|
|
62
|
+
email: "jo@example.com", firstName: "Jo",
|
|
63
|
+
customFields: { level: "member" },
|
|
64
|
+
redirectUrl: "https://club.example.com/sign-in", // the site's sign-in page (one of its own addresses)
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The email's link works once for 7 days and opens the sign-in page with `mode=magicLink&token=…`: handle
|
|
69
|
+
it with `SL.authKit.verifyMagicLink(clientId, token)`, as for any magic link. After that the member
|
|
70
|
+
signs in however the site offers (a magic link to their email, "forgot password" to set one, Google
|
|
71
|
+
with the same email). Build the sign-in page with no "Create an account" option when the kit's
|
|
72
|
+
`signUp` is `invite` (it's on the public config, `SL.authKit.load(authKitId)`), and say "Members only.
|
|
73
|
+
Ask the club for an invitation."
|
|
74
|
+
|
|
75
|
+
**Added by the staff first?** When someone signs in with a verified email or phone, their login is
|
|
76
|
+
attached to the contact the site already has for it: an invite, someone the staff added in Studio, a
|
|
77
|
+
form they filled in. So the level set on that contact is theirs from the first sign-in.
|
|
78
|
+
|
|
79
|
+
## 2. The level field
|
|
80
|
+
|
|
81
|
+
In a **website**, contact fields are part of its cloud blueprint (`cloud/contacts.json`, applied with
|
|
82
|
+
`forge-cloud`):
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"fields": {
|
|
87
|
+
"level": { "title": "Membership", "type": "string", "enum": ["pending", "member", "gold"], "public": "visible" },
|
|
88
|
+
"memberUntil": { "title": "Member until", "type": "date", "public": "visible" },
|
|
89
|
+
"showInDirectory": { "title": "Show me in the members' list", "type": "boolean", "public": "editable" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| `public` | Member can see it | Member can change it | Use for |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `hidden` (default) | no | no | Staff notes, a paid-until the member shouldn't see |
|
|
97
|
+
| `visible` | yes | **no** | **The level**, an expiry date |
|
|
98
|
+
| `editable` | yes | yes | Preferences and consent the member owns |
|
|
99
|
+
|
|
100
|
+
A member's own reads and writes (`SL.contact.publicGetMine`, `SL.contact.publicUpdateMine`) and public
|
|
101
|
+
forms follow these settings on the server, so a `visible` level can't be changed from the browser.
|
|
102
|
+
|
|
103
|
+
Outside a website (a plain app), the same settings live in the collection's `contactCustomFields`
|
|
104
|
+
(field definitions, a SchemaFormConfig) and `contacts` settings groups (`publicVisibleFields`,
|
|
105
|
+
`publicEditableFields`, paths like `customFields.level`). Those lists replace what's stored: read
|
|
106
|
+
them, add to them, and write back the whole list, defaults included. Prefer letting the owner set
|
|
107
|
+
fields up in Studio → Customers.
|
|
108
|
+
|
|
109
|
+
## 3. Showing members their status
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const me = await SL.contact.publicGetMine(collectionId) // needs the member's session
|
|
113
|
+
// me.contact?.customFields?.level → "gold"; only fields the member may see are returned
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use it for the "My membership" page and to decide what to **show**. It is not a lock: anything
|
|
117
|
+
members-only must come from a server function.
|
|
118
|
+
|
|
119
|
+
## 4. Members-only content: check in a server function
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// visibility: public, authority: collection, elevated: true
|
|
123
|
+
// capabilities: sl:contacts:write (forCaller), sl:records:read
|
|
124
|
+
export async function memberNews(ctx, event) {
|
|
125
|
+
const me = await ctx.sl.contacts.forCaller() // null when signed out
|
|
126
|
+
if (!me) return { ok: false, reason: "sign-in" }
|
|
127
|
+
const level = me.customFields.level
|
|
128
|
+
if (level !== "member" && level !== "gold") return { ok: false, reason: "not-a-member" }
|
|
129
|
+
const until = me.customFields.memberUntil
|
|
130
|
+
if (until && String(until) < new Date().toISOString().slice(0, 10)) return { ok: false, reason: "expired" }
|
|
131
|
+
const news = await ctx.sl.appRecords.query({ recordType: "member-news" })
|
|
132
|
+
return { ok: true, level, news } // never return me.contactId or staff-only fields
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- `forCaller()` returns the member's contact with its tags and **all** custom fields, staff-only ones
|
|
137
|
+
included. Decide with them and return only what the member may see.
|
|
138
|
+
- A site's `callFunction` sends the member's session with the call. Without it, the function sees an
|
|
139
|
+
anonymous caller. Check that `src/site/lib/call-function.ts` adds `authorization: Bearer <token>`
|
|
140
|
+
from `sl().getBearerToken()`; older sites may need it added.
|
|
141
|
+
- Do the same check in every function that acts for members: booking a court, entering a result,
|
|
142
|
+
claiming a discount.
|
|
143
|
+
|
|
144
|
+
## 5. Joining rules
|
|
145
|
+
|
|
146
|
+
| Club | How it works |
|
|
147
|
+
|---|---|
|
|
148
|
+
| **Open** (sign up = member) | Signed in is enough; no level needed, or a `level` the site never gates on |
|
|
149
|
+
| **Approval** ("sign up and I'll add you") | Anyone signs up; the owner sets `level` in Studio → Customers; until then the site says "We'll confirm your membership" |
|
|
150
|
+
| **Invite only** (no sign-up) | `signUp: 'invite'`; the owner invites people with their level (`SL.authKit.invite`): the Customer Login app's Members card, or an "Invite a member" screen on the site's admin page; the sign-in page has no sign-up |
|
|
151
|
+
| **Tiers** (silver, gold) | `level` with an `enum`; each gated function lists the levels it allows |
|
|
152
|
+
| **Paid** | By hand: the owner sets `level` (and `memberUntil`) once paid. Online: a memberships app or connector writes the same fields after payment |
|
|
153
|
+
|
|
154
|
+
Record joining as an interaction (`ctx.sl.interactions.record`, e.g. `member-joined`), and a
|
|
155
|
+
`cloud/flows.json` flow can welcome the member and tell the team someone is waiting for approval.
|
|
156
|
+
|
|
157
|
+
## 6. The owner's side
|
|
158
|
+
|
|
159
|
+
- **Studio → Customers**: everyone who signed up, their level, and when they joined. Filter by level
|
|
160
|
+
and set it on the contact.
|
|
161
|
+
- **Inviting**: the Customer Login app's Members card (Who can join, Invite a member), or the site's own
|
|
162
|
+
admin screen calling `SL.authKit.invite`; inviting someone again sends a fresh link.
|
|
163
|
+
- **Email a level**: a segment on `customFields.level`, sent from Studio (comms.md).
|
|
164
|
+
- **Tags** suit many-valued labels (`committee`, `coach`); a member has one `level`.
|
|
165
|
+
|
|
166
|
+
## Don't
|
|
167
|
+
|
|
168
|
+
- Keep members in app records, a table of your own, or the site's code. The contact is the member.
|
|
169
|
+
- Trust the browser's idea of the level. Check it in a server function.
|
|
170
|
+
- Let a member set a level that unlocks anything (`public: "editable"`).
|
|
171
|
+
- Return contact ids, other members' details, or staff-only fields to the browser.
|
|
172
|
+
- Build payments into a site. A memberships app or connector owns fees.
|
|
173
|
+
- Hide the sign-up button and call that invitation only. Set `signUp: 'invite'`, so the server refuses.
|
|
174
|
+
|
|
175
|
+
## Example: a tennis box league
|
|
176
|
+
|
|
177
|
+
- **Players** are members: own logins, `level` = `pending` or `player`, set by the organiser.
|
|
178
|
+
- **Fixtures, results and standings** are a custom app's records.
|
|
179
|
+
- Only a `player` can enter a result: the function checks `forCaller()`, then that the player is in
|
|
180
|
+
that fixture.
|
|
181
|
+
- **Standings** are public; contact details stay hidden.
|
|
182
|
+
- **New player signs up**: the flow emails the organiser, who sets them to `player` in Studio.
|