@nerdfolio/ba-guest-list 0.0.1
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/README.md +96 -0
- package/dist/client.cjs +38 -0
- package/dist/client.d.cts +10 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.mjs +13 -0
- package/dist/index.cjs +188 -0
- package/dist/index.d.cts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.mjs +163 -0
- package/package.json +44 -0
- package/src/client.ts +12 -0
- package/src/index.ts +205 -0
- package/src/utils.ts +8 -0
- package/tsup.config.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Better Auth Guest List
|
|
2
|
+
Plugin to provide fixed guest list functionality. Intended use is for development testing and possibly demos with
|
|
3
|
+
know login names, e.g. login as "Alice" or "Bob".
|
|
4
|
+
|
|
5
|
+
You can use it for logging in by just typing a single-word name or binding that name to a UI button.
|
|
6
|
+
|
|
7
|
+
The fixed guest list can be defined on the server-side with roles so this plugin
|
|
8
|
+
can also be used for testing roles.
|
|
9
|
+
|
|
10
|
+
As this plugin is only intended to be a temporary aid to development and demo, it does not add any field to the schema. Internally, the guest name is transformed into an email via a fixed template, e.g. `tom.onguestlist@emaildomain` and that is the way that user will be looked up.
|
|
11
|
+
|
|
12
|
+
# Installation
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
|
|
16
|
+
pnpm add @nerdfolio/ba-guest-list
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
# Usage
|
|
21
|
+
|
|
22
|
+
## Server-side setup
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// auth.ts
|
|
26
|
+
import { betterAuth } from "better-auth"
|
|
27
|
+
import { guestList } from "@nerdfolio/ba-guest-list"
|
|
28
|
+
|
|
29
|
+
export const auth = betterAuth({
|
|
30
|
+
...otherConfigs,
|
|
31
|
+
plugins: [
|
|
32
|
+
...otherPlugins,
|
|
33
|
+
guestList({
|
|
34
|
+
allowGuests: [
|
|
35
|
+
// can be array of names or array of {name: string, role?: comma-separated-string}
|
|
36
|
+
{ name: "Alice", role: "admin" },
|
|
37
|
+
{ name: "Bob", role: "user" },
|
|
38
|
+
{ name: "Charlie", role: "user" },
|
|
39
|
+
],
|
|
40
|
+
revealNames: true // whether the client can see this list (useful for demos)
|
|
41
|
+
})
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
|
|
49
|
+
`allowGuests`: can be an array of names or array of `{name: string, role?: string}`. role uses better-auth convention as a comma-separated string of actual roles.
|
|
50
|
+
|
|
51
|
+
`revealNames`: is a boolean. When enabled, the client will be able to retrieve the guest names via `client.signIn.guestList.reveal()`. Names may also be returned in api errors during logins. When undefined or disabled, the `reveal()` endpoint will just return `null` and names will not be sent in error messages.
|
|
52
|
+
|
|
53
|
+
`emailDomainName`: optional. Internally this plugin generates a fake email based on the guest name. It detects the app's domain and use that for email generation. You can override that with this option.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Client-side setup
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
|
|
60
|
+
import { guestListClient } from "@nerdfolio/ba-guest-list/client"
|
|
61
|
+
|
|
62
|
+
export const authClient = createAuthClient({
|
|
63
|
+
plugins: [
|
|
64
|
+
guestListClient()
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Signin
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
|
|
74
|
+
// authClient is the better-auth client as defined in client-side setup
|
|
75
|
+
// GUEST_NAME has to be in the list of names defined in server-side setup, otherwise login will fail
|
|
76
|
+
authClient.signIn.guestList({
|
|
77
|
+
name: GUEST_NAME
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If you have enabled `revealNames` in your server-side setup, you can retrieve that list of names on the client side. This may be useful for creating demos with a fixed list of login names. The plugin will
|
|
82
|
+
call a special fetch endpoint and returns either `null` or an array of name strings. When `revealNames` is undefined or false, it returns `null`.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// just an async so you'll need to use it according to the way your frontend framework andles async
|
|
86
|
+
|
|
87
|
+
const guestNames = await authClient.signIn.guestList.reveal()
|
|
88
|
+
.then(({ data, error: _e }) => data?.join(", "))
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// for example, in Solidstart, you can retrieve this via createAsync
|
|
92
|
+
const guestList = createAsync(async () =>
|
|
93
|
+
authClient.signIn.guestList.reveal().then(({ data, error: _e }) => data?.join(", "))
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
```
|
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
guestListClient: () => guestListClient
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(client_exports);
|
|
26
|
+
var guestListClient = () => {
|
|
27
|
+
return {
|
|
28
|
+
id: "guest-list",
|
|
29
|
+
$InferServerPlugin: {}
|
|
30
|
+
// pathMethods: {
|
|
31
|
+
// "/sign-in/guest-list": "POST",
|
|
32
|
+
// },
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
36
|
+
0 && (module.exports = {
|
|
37
|
+
guestListClient
|
|
38
|
+
});
|
package/dist/client.d.ts
ADDED
package/dist/client.mjs
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
guestList: () => guestList
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var import_api = require("better-auth/api");
|
|
27
|
+
var import_cookies = require("better-auth/cookies");
|
|
28
|
+
var import_lodash_es = require("lodash-es");
|
|
29
|
+
var import_v4_mini = require("zod/v4-mini");
|
|
30
|
+
|
|
31
|
+
// src/utils.ts
|
|
32
|
+
function getOrigin(url) {
|
|
33
|
+
try {
|
|
34
|
+
const parsedUrl = new URL(url);
|
|
35
|
+
return parsedUrl.origin;
|
|
36
|
+
} catch (_error) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/index.ts
|
|
42
|
+
function formatName(name) {
|
|
43
|
+
return (0, import_lodash_es.capitalize)(name.replaceAll(/\s/g, ""));
|
|
44
|
+
}
|
|
45
|
+
var guestList = (options) => {
|
|
46
|
+
const ERROR_CODES = {
|
|
47
|
+
NAME_NOT_PROVIDED: "Guest name not provided",
|
|
48
|
+
NAME_NOT_ON_GUEST_LIST: "Your name is not on the guest list",
|
|
49
|
+
NAME_ONE_WORD_ONLY: "Please only use 1-word names",
|
|
50
|
+
FAILED_TO_CREATE_USER: "Failed to create user",
|
|
51
|
+
COULD_NOT_CREATE_SESSION: "Could not create session"
|
|
52
|
+
};
|
|
53
|
+
const guestLookup = Object.fromEntries(
|
|
54
|
+
(options?.allowGuests ?? []).map((entry) => typeof entry === "string" ? { name: entry, role: "" } : entry).filter((entry) => !!entry && entry.name).map(({ name, role }) => [formatName(name), {
|
|
55
|
+
name: formatName(name),
|
|
56
|
+
role: (role ?? "").split(",").map((s) => s.trim()).join(",")
|
|
57
|
+
}])
|
|
58
|
+
);
|
|
59
|
+
return {
|
|
60
|
+
id: "guest-list",
|
|
61
|
+
endpoints: {
|
|
62
|
+
signInGuest: (0, import_api.createAuthEndpoint)(
|
|
63
|
+
"/sign-in/guest-list",
|
|
64
|
+
{
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: import_v4_mini.z.object({
|
|
67
|
+
name: import_v4_mini.z.string()
|
|
68
|
+
}),
|
|
69
|
+
metadata: {
|
|
70
|
+
openapi: {
|
|
71
|
+
description: "Sign in as a guest with name only",
|
|
72
|
+
responses: {
|
|
73
|
+
200: {
|
|
74
|
+
description: "Sign in as a guest successful",
|
|
75
|
+
content: {
|
|
76
|
+
"application/json": {
|
|
77
|
+
schema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
user: {
|
|
81
|
+
$ref: "#/components/schemas/User"
|
|
82
|
+
},
|
|
83
|
+
session: {
|
|
84
|
+
$ref: "#/components/schemas/Session"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
async (ctx) => {
|
|
96
|
+
const { name } = ctx.body;
|
|
97
|
+
if (!name) {
|
|
98
|
+
ctx.context.logger.error("Guest name not provided");
|
|
99
|
+
throw new import_api.APIError("UNAUTHORIZED", {
|
|
100
|
+
message: options?.revealNames ? `Guest name not provided. Try: ${JSON.stringify(Object.keys(guestLookup))}` : ERROR_CODES.NAME_NOT_PROVIDED
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (name.trim().split(/\s+/).length > 1) {
|
|
104
|
+
ctx.context.logger.error("For simplicity, only one word names are allowed");
|
|
105
|
+
throw new import_api.APIError("UNAUTHORIZED", {
|
|
106
|
+
message: ERROR_CODES.NAME_ONE_WORD_ONLY
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const cleanedName = formatName(name);
|
|
110
|
+
if (!guestLookup[cleanedName]) {
|
|
111
|
+
throw new import_api.APIError("UNAUTHORIZED", {
|
|
112
|
+
message: options?.revealNames ? `Name not on list. Try: ${JSON.stringify(Object.keys(guestLookup))}` : ERROR_CODES.NAME_NOT_ON_GUEST_LIST
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const { emailDomainName = getOrigin(ctx.context.baseURL) } = options ?? {};
|
|
116
|
+
const email = `${cleanedName.toLowerCase().replaceAll(/\s/g, "")}.onguestlist@${emailDomainName}`;
|
|
117
|
+
const found = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
118
|
+
async function createNewUser() {
|
|
119
|
+
const newUser = await ctx.context.internalAdapter.createUser(
|
|
120
|
+
{
|
|
121
|
+
email,
|
|
122
|
+
emailVerified: false,
|
|
123
|
+
name: cleanedName,
|
|
124
|
+
role: guestLookup[cleanedName].role,
|
|
125
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
126
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
127
|
+
},
|
|
128
|
+
ctx
|
|
129
|
+
);
|
|
130
|
+
if (!newUser) {
|
|
131
|
+
throw ctx.error("INTERNAL_SERVER_ERROR", {
|
|
132
|
+
message: ERROR_CODES.FAILED_TO_CREATE_USER
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return newUser;
|
|
136
|
+
}
|
|
137
|
+
const user = found ? found.user : await createNewUser();
|
|
138
|
+
const session = await ctx.context.internalAdapter.createSession(user.id, ctx, true);
|
|
139
|
+
if (!session) {
|
|
140
|
+
return ctx.json(null, {
|
|
141
|
+
status: 400,
|
|
142
|
+
body: {
|
|
143
|
+
message: ERROR_CODES.COULD_NOT_CREATE_SESSION
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
await (0, import_cookies.setSessionCookie)(ctx, { session, user });
|
|
148
|
+
return ctx.json({ token: session.token, user });
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
revealGuestList: (0, import_api.createAuthEndpoint)(
|
|
152
|
+
"/sign-in/guest-list/reveal",
|
|
153
|
+
{
|
|
154
|
+
method: "GET",
|
|
155
|
+
metadata: {
|
|
156
|
+
openapi: {
|
|
157
|
+
description: "Reveal guest list if 'revealNames' is enabled. Empty array otherwise",
|
|
158
|
+
responses: {
|
|
159
|
+
200: {
|
|
160
|
+
description: "List of allowed guest names or empty array",
|
|
161
|
+
content: {
|
|
162
|
+
"application/json": {
|
|
163
|
+
schema: {
|
|
164
|
+
type: "array",
|
|
165
|
+
items: {
|
|
166
|
+
type: "string"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
async (ctx) => {
|
|
177
|
+
return ctx.json(options?.revealNames ? Object.keys(guestLookup) : []);
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
},
|
|
181
|
+
//schema: mergeSchema(schema, options?.schema),
|
|
182
|
+
$ERROR_CODES: ERROR_CODES
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
186
|
+
0 && (module.exports = {
|
|
187
|
+
guestList
|
|
188
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as better_auth from 'better-auth';
|
|
2
|
+
import { z } from 'zod/v4-mini';
|
|
3
|
+
|
|
4
|
+
type GuestWithRole = {
|
|
5
|
+
name: string;
|
|
6
|
+
role: string;
|
|
7
|
+
};
|
|
8
|
+
interface GuestListOptions {
|
|
9
|
+
/**
|
|
10
|
+
* List of accepted guest names
|
|
11
|
+
*/
|
|
12
|
+
allowGuests: string[] | GuestWithRole[];
|
|
13
|
+
/**
|
|
14
|
+
* When true returns the list of guest names via the guestList.reveal() endpoint and via errors.
|
|
15
|
+
* When false returns nothing.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
revealNames?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Configure the domain name of the temporary email
|
|
21
|
+
* address for the guest users in the database.
|
|
22
|
+
* @default "baseURL"
|
|
23
|
+
*/
|
|
24
|
+
emailDomainName?: string;
|
|
25
|
+
}
|
|
26
|
+
declare const guestList: (options?: GuestListOptions) => {
|
|
27
|
+
id: "guest-list";
|
|
28
|
+
endpoints: {
|
|
29
|
+
signInGuest: {
|
|
30
|
+
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
|
|
31
|
+
body: {
|
|
32
|
+
name: string;
|
|
33
|
+
};
|
|
34
|
+
} & {
|
|
35
|
+
method?: "POST" | undefined;
|
|
36
|
+
} & {
|
|
37
|
+
query?: Record<string, any> | undefined;
|
|
38
|
+
} & {
|
|
39
|
+
params?: Record<string, any>;
|
|
40
|
+
} & {
|
|
41
|
+
request?: Request;
|
|
42
|
+
} & {
|
|
43
|
+
headers?: HeadersInit;
|
|
44
|
+
} & {
|
|
45
|
+
asResponse?: boolean;
|
|
46
|
+
returnHeaders?: boolean;
|
|
47
|
+
use?: better_auth.Middleware[];
|
|
48
|
+
path?: string;
|
|
49
|
+
} & {
|
|
50
|
+
asResponse?: AsResponse | undefined;
|
|
51
|
+
returnHeaders?: ReturnHeaders | undefined;
|
|
52
|
+
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
|
|
53
|
+
headers: Headers;
|
|
54
|
+
response: {
|
|
55
|
+
token: string;
|
|
56
|
+
user: {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
emailVerified: boolean;
|
|
60
|
+
email: string;
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
updatedAt: Date;
|
|
63
|
+
image?: string | null | undefined;
|
|
64
|
+
};
|
|
65
|
+
} | null;
|
|
66
|
+
} : {
|
|
67
|
+
token: string;
|
|
68
|
+
user: {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
emailVerified: boolean;
|
|
72
|
+
email: string;
|
|
73
|
+
createdAt: Date;
|
|
74
|
+
updatedAt: Date;
|
|
75
|
+
image?: string | null | undefined;
|
|
76
|
+
};
|
|
77
|
+
} | null>;
|
|
78
|
+
options: {
|
|
79
|
+
method: "POST";
|
|
80
|
+
body: z.ZodMiniObject<{
|
|
81
|
+
name: z.ZodMiniString<string>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
metadata: {
|
|
84
|
+
openapi: {
|
|
85
|
+
description: string;
|
|
86
|
+
responses: {
|
|
87
|
+
200: {
|
|
88
|
+
description: string;
|
|
89
|
+
content: {
|
|
90
|
+
"application/json": {
|
|
91
|
+
schema: {
|
|
92
|
+
type: "object";
|
|
93
|
+
properties: {
|
|
94
|
+
user: {
|
|
95
|
+
$ref: string;
|
|
96
|
+
};
|
|
97
|
+
session: {
|
|
98
|
+
$ref: string;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
} & {
|
|
109
|
+
use: any[];
|
|
110
|
+
};
|
|
111
|
+
path: "/sign-in/guest-list";
|
|
112
|
+
};
|
|
113
|
+
revealGuestList: {
|
|
114
|
+
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({
|
|
115
|
+
body?: undefined;
|
|
116
|
+
} & {
|
|
117
|
+
method?: "GET" | undefined;
|
|
118
|
+
} & {
|
|
119
|
+
query?: Record<string, any> | undefined;
|
|
120
|
+
} & {
|
|
121
|
+
params?: Record<string, any>;
|
|
122
|
+
} & {
|
|
123
|
+
request?: Request;
|
|
124
|
+
} & {
|
|
125
|
+
headers?: HeadersInit;
|
|
126
|
+
} & {
|
|
127
|
+
asResponse?: boolean;
|
|
128
|
+
returnHeaders?: boolean;
|
|
129
|
+
use?: better_auth.Middleware[];
|
|
130
|
+
path?: string;
|
|
131
|
+
} & {
|
|
132
|
+
asResponse?: AsResponse | undefined;
|
|
133
|
+
returnHeaders?: ReturnHeaders | undefined;
|
|
134
|
+
}) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
|
|
135
|
+
headers: Headers;
|
|
136
|
+
response: string[];
|
|
137
|
+
} : string[]>;
|
|
138
|
+
options: {
|
|
139
|
+
method: "GET";
|
|
140
|
+
metadata: {
|
|
141
|
+
openapi: {
|
|
142
|
+
description: string;
|
|
143
|
+
responses: {
|
|
144
|
+
200: {
|
|
145
|
+
description: string;
|
|
146
|
+
content: {
|
|
147
|
+
"application/json": {
|
|
148
|
+
schema: {
|
|
149
|
+
type: "array";
|
|
150
|
+
items: {
|
|
151
|
+
type: string;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
} & {
|
|
161
|
+
use: any[];
|
|
162
|
+
};
|
|
163
|
+
path: "/sign-in/guest-list/reveal";
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
$ERROR_CODES: {
|
|
167
|
+
readonly NAME_NOT_PROVIDED: "Guest name not provided";
|
|
168
|
+
readonly NAME_NOT_ON_GUEST_LIST: "Your name is not on the guest list";
|
|
169
|
+
readonly NAME_ONE_WORD_ONLY: "Please only use 1-word names";
|
|
170
|
+
readonly FAILED_TO_CREATE_USER: "Failed to create user";
|
|
171
|
+
readonly COULD_NOT_CREATE_SESSION: "Could not create session";
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export { type GuestListOptions, guestList };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as better_auth from 'better-auth';
|
|
2
|
+
import { z } from 'zod/v4-mini';
|
|
3
|
+
|
|
4
|
+
type GuestWithRole = {
|
|
5
|
+
name: string;
|
|
6
|
+
role: string;
|
|
7
|
+
};
|
|
8
|
+
interface GuestListOptions {
|
|
9
|
+
/**
|
|
10
|
+
* List of accepted guest names
|
|
11
|
+
*/
|
|
12
|
+
allowGuests: string[] | GuestWithRole[];
|
|
13
|
+
/**
|
|
14
|
+
* When true returns the list of guest names via the guestList.reveal() endpoint and via errors.
|
|
15
|
+
* When false returns nothing.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
revealNames?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Configure the domain name of the temporary email
|
|
21
|
+
* address for the guest users in the database.
|
|
22
|
+
* @default "baseURL"
|
|
23
|
+
*/
|
|
24
|
+
emailDomainName?: string;
|
|
25
|
+
}
|
|
26
|
+
declare const guestList: (options?: GuestListOptions) => {
|
|
27
|
+
id: "guest-list";
|
|
28
|
+
endpoints: {
|
|
29
|
+
signInGuest: {
|
|
30
|
+
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
|
|
31
|
+
body: {
|
|
32
|
+
name: string;
|
|
33
|
+
};
|
|
34
|
+
} & {
|
|
35
|
+
method?: "POST" | undefined;
|
|
36
|
+
} & {
|
|
37
|
+
query?: Record<string, any> | undefined;
|
|
38
|
+
} & {
|
|
39
|
+
params?: Record<string, any>;
|
|
40
|
+
} & {
|
|
41
|
+
request?: Request;
|
|
42
|
+
} & {
|
|
43
|
+
headers?: HeadersInit;
|
|
44
|
+
} & {
|
|
45
|
+
asResponse?: boolean;
|
|
46
|
+
returnHeaders?: boolean;
|
|
47
|
+
use?: better_auth.Middleware[];
|
|
48
|
+
path?: string;
|
|
49
|
+
} & {
|
|
50
|
+
asResponse?: AsResponse | undefined;
|
|
51
|
+
returnHeaders?: ReturnHeaders | undefined;
|
|
52
|
+
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
|
|
53
|
+
headers: Headers;
|
|
54
|
+
response: {
|
|
55
|
+
token: string;
|
|
56
|
+
user: {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
emailVerified: boolean;
|
|
60
|
+
email: string;
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
updatedAt: Date;
|
|
63
|
+
image?: string | null | undefined;
|
|
64
|
+
};
|
|
65
|
+
} | null;
|
|
66
|
+
} : {
|
|
67
|
+
token: string;
|
|
68
|
+
user: {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
emailVerified: boolean;
|
|
72
|
+
email: string;
|
|
73
|
+
createdAt: Date;
|
|
74
|
+
updatedAt: Date;
|
|
75
|
+
image?: string | null | undefined;
|
|
76
|
+
};
|
|
77
|
+
} | null>;
|
|
78
|
+
options: {
|
|
79
|
+
method: "POST";
|
|
80
|
+
body: z.ZodMiniObject<{
|
|
81
|
+
name: z.ZodMiniString<string>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
metadata: {
|
|
84
|
+
openapi: {
|
|
85
|
+
description: string;
|
|
86
|
+
responses: {
|
|
87
|
+
200: {
|
|
88
|
+
description: string;
|
|
89
|
+
content: {
|
|
90
|
+
"application/json": {
|
|
91
|
+
schema: {
|
|
92
|
+
type: "object";
|
|
93
|
+
properties: {
|
|
94
|
+
user: {
|
|
95
|
+
$ref: string;
|
|
96
|
+
};
|
|
97
|
+
session: {
|
|
98
|
+
$ref: string;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
} & {
|
|
109
|
+
use: any[];
|
|
110
|
+
};
|
|
111
|
+
path: "/sign-in/guest-list";
|
|
112
|
+
};
|
|
113
|
+
revealGuestList: {
|
|
114
|
+
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({
|
|
115
|
+
body?: undefined;
|
|
116
|
+
} & {
|
|
117
|
+
method?: "GET" | undefined;
|
|
118
|
+
} & {
|
|
119
|
+
query?: Record<string, any> | undefined;
|
|
120
|
+
} & {
|
|
121
|
+
params?: Record<string, any>;
|
|
122
|
+
} & {
|
|
123
|
+
request?: Request;
|
|
124
|
+
} & {
|
|
125
|
+
headers?: HeadersInit;
|
|
126
|
+
} & {
|
|
127
|
+
asResponse?: boolean;
|
|
128
|
+
returnHeaders?: boolean;
|
|
129
|
+
use?: better_auth.Middleware[];
|
|
130
|
+
path?: string;
|
|
131
|
+
} & {
|
|
132
|
+
asResponse?: AsResponse | undefined;
|
|
133
|
+
returnHeaders?: ReturnHeaders | undefined;
|
|
134
|
+
}) | undefined): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
|
|
135
|
+
headers: Headers;
|
|
136
|
+
response: string[];
|
|
137
|
+
} : string[]>;
|
|
138
|
+
options: {
|
|
139
|
+
method: "GET";
|
|
140
|
+
metadata: {
|
|
141
|
+
openapi: {
|
|
142
|
+
description: string;
|
|
143
|
+
responses: {
|
|
144
|
+
200: {
|
|
145
|
+
description: string;
|
|
146
|
+
content: {
|
|
147
|
+
"application/json": {
|
|
148
|
+
schema: {
|
|
149
|
+
type: "array";
|
|
150
|
+
items: {
|
|
151
|
+
type: string;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
} & {
|
|
161
|
+
use: any[];
|
|
162
|
+
};
|
|
163
|
+
path: "/sign-in/guest-list/reveal";
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
$ERROR_CODES: {
|
|
167
|
+
readonly NAME_NOT_PROVIDED: "Guest name not provided";
|
|
168
|
+
readonly NAME_NOT_ON_GUEST_LIST: "Your name is not on the guest list";
|
|
169
|
+
readonly NAME_ONE_WORD_ONLY: "Please only use 1-word names";
|
|
170
|
+
readonly FAILED_TO_CREATE_USER: "Failed to create user";
|
|
171
|
+
readonly COULD_NOT_CREATE_SESSION: "Could not create session";
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export { type GuestListOptions, guestList };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { APIError, createAuthEndpoint } from "better-auth/api";
|
|
3
|
+
import { setSessionCookie } from "better-auth/cookies";
|
|
4
|
+
import { capitalize } from "lodash-es";
|
|
5
|
+
import { z } from "zod/v4-mini";
|
|
6
|
+
|
|
7
|
+
// src/utils.ts
|
|
8
|
+
function getOrigin(url) {
|
|
9
|
+
try {
|
|
10
|
+
const parsedUrl = new URL(url);
|
|
11
|
+
return parsedUrl.origin;
|
|
12
|
+
} catch (_error) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/index.ts
|
|
18
|
+
function formatName(name) {
|
|
19
|
+
return capitalize(name.replaceAll(/\s/g, ""));
|
|
20
|
+
}
|
|
21
|
+
var guestList = (options) => {
|
|
22
|
+
const ERROR_CODES = {
|
|
23
|
+
NAME_NOT_PROVIDED: "Guest name not provided",
|
|
24
|
+
NAME_NOT_ON_GUEST_LIST: "Your name is not on the guest list",
|
|
25
|
+
NAME_ONE_WORD_ONLY: "Please only use 1-word names",
|
|
26
|
+
FAILED_TO_CREATE_USER: "Failed to create user",
|
|
27
|
+
COULD_NOT_CREATE_SESSION: "Could not create session"
|
|
28
|
+
};
|
|
29
|
+
const guestLookup = Object.fromEntries(
|
|
30
|
+
(options?.allowGuests ?? []).map((entry) => typeof entry === "string" ? { name: entry, role: "" } : entry).filter((entry) => !!entry && entry.name).map(({ name, role }) => [formatName(name), {
|
|
31
|
+
name: formatName(name),
|
|
32
|
+
role: (role ?? "").split(",").map((s) => s.trim()).join(",")
|
|
33
|
+
}])
|
|
34
|
+
);
|
|
35
|
+
return {
|
|
36
|
+
id: "guest-list",
|
|
37
|
+
endpoints: {
|
|
38
|
+
signInGuest: createAuthEndpoint(
|
|
39
|
+
"/sign-in/guest-list",
|
|
40
|
+
{
|
|
41
|
+
method: "POST",
|
|
42
|
+
body: z.object({
|
|
43
|
+
name: z.string()
|
|
44
|
+
}),
|
|
45
|
+
metadata: {
|
|
46
|
+
openapi: {
|
|
47
|
+
description: "Sign in as a guest with name only",
|
|
48
|
+
responses: {
|
|
49
|
+
200: {
|
|
50
|
+
description: "Sign in as a guest successful",
|
|
51
|
+
content: {
|
|
52
|
+
"application/json": {
|
|
53
|
+
schema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
user: {
|
|
57
|
+
$ref: "#/components/schemas/User"
|
|
58
|
+
},
|
|
59
|
+
session: {
|
|
60
|
+
$ref: "#/components/schemas/Session"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async (ctx) => {
|
|
72
|
+
const { name } = ctx.body;
|
|
73
|
+
if (!name) {
|
|
74
|
+
ctx.context.logger.error("Guest name not provided");
|
|
75
|
+
throw new APIError("UNAUTHORIZED", {
|
|
76
|
+
message: options?.revealNames ? `Guest name not provided. Try: ${JSON.stringify(Object.keys(guestLookup))}` : ERROR_CODES.NAME_NOT_PROVIDED
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (name.trim().split(/\s+/).length > 1) {
|
|
80
|
+
ctx.context.logger.error("For simplicity, only one word names are allowed");
|
|
81
|
+
throw new APIError("UNAUTHORIZED", {
|
|
82
|
+
message: ERROR_CODES.NAME_ONE_WORD_ONLY
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const cleanedName = formatName(name);
|
|
86
|
+
if (!guestLookup[cleanedName]) {
|
|
87
|
+
throw new APIError("UNAUTHORIZED", {
|
|
88
|
+
message: options?.revealNames ? `Name not on list. Try: ${JSON.stringify(Object.keys(guestLookup))}` : ERROR_CODES.NAME_NOT_ON_GUEST_LIST
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const { emailDomainName = getOrigin(ctx.context.baseURL) } = options ?? {};
|
|
92
|
+
const email = `${cleanedName.toLowerCase().replaceAll(/\s/g, "")}.onguestlist@${emailDomainName}`;
|
|
93
|
+
const found = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
94
|
+
async function createNewUser() {
|
|
95
|
+
const newUser = await ctx.context.internalAdapter.createUser(
|
|
96
|
+
{
|
|
97
|
+
email,
|
|
98
|
+
emailVerified: false,
|
|
99
|
+
name: cleanedName,
|
|
100
|
+
role: guestLookup[cleanedName].role,
|
|
101
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
102
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
103
|
+
},
|
|
104
|
+
ctx
|
|
105
|
+
);
|
|
106
|
+
if (!newUser) {
|
|
107
|
+
throw ctx.error("INTERNAL_SERVER_ERROR", {
|
|
108
|
+
message: ERROR_CODES.FAILED_TO_CREATE_USER
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return newUser;
|
|
112
|
+
}
|
|
113
|
+
const user = found ? found.user : await createNewUser();
|
|
114
|
+
const session = await ctx.context.internalAdapter.createSession(user.id, ctx, true);
|
|
115
|
+
if (!session) {
|
|
116
|
+
return ctx.json(null, {
|
|
117
|
+
status: 400,
|
|
118
|
+
body: {
|
|
119
|
+
message: ERROR_CODES.COULD_NOT_CREATE_SESSION
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
await setSessionCookie(ctx, { session, user });
|
|
124
|
+
return ctx.json({ token: session.token, user });
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
revealGuestList: createAuthEndpoint(
|
|
128
|
+
"/sign-in/guest-list/reveal",
|
|
129
|
+
{
|
|
130
|
+
method: "GET",
|
|
131
|
+
metadata: {
|
|
132
|
+
openapi: {
|
|
133
|
+
description: "Reveal guest list if 'revealNames' is enabled. Empty array otherwise",
|
|
134
|
+
responses: {
|
|
135
|
+
200: {
|
|
136
|
+
description: "List of allowed guest names or empty array",
|
|
137
|
+
content: {
|
|
138
|
+
"application/json": {
|
|
139
|
+
schema: {
|
|
140
|
+
type: "array",
|
|
141
|
+
items: {
|
|
142
|
+
type: "string"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
async (ctx) => {
|
|
153
|
+
return ctx.json(options?.revealNames ? Object.keys(guestLookup) : []);
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
//schema: mergeSchema(schema, options?.schema),
|
|
158
|
+
$ERROR_CODES: ERROR_CODES
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
export {
|
|
162
|
+
guestList
|
|
163
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nerdfolio/ba-guest-list",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Similar to anonymous, but with a name that must be on a guest list. Useful for testing or demo with fixed logins",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"require": "./dist/index.cjs"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"types": "./dist/client.d.ts",
|
|
14
|
+
"import": "./dist/client.mjs",
|
|
15
|
+
"require": "./dist/client.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"better-auth",
|
|
20
|
+
"better-auth plugin",
|
|
21
|
+
"guest login",
|
|
22
|
+
"demo login"
|
|
23
|
+
],
|
|
24
|
+
"author": "taivo@github",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/lodash-es": "^4.17.12",
|
|
31
|
+
"better-auth": "1.2.9"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"better-auth": "1.2.9"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"lodash-es": "^4.17.21",
|
|
38
|
+
"zod": "^3.25.67"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
42
|
+
"build": "tsup-node"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BetterAuthClientPlugin } from "better-auth"
|
|
2
|
+
import type { guestList } from "."
|
|
3
|
+
|
|
4
|
+
export const guestListClient = () => {
|
|
5
|
+
return {
|
|
6
|
+
id: "guest-list",
|
|
7
|
+
$InferServerPlugin: {} as ReturnType<typeof guestList>,
|
|
8
|
+
// pathMethods: {
|
|
9
|
+
// "/sign-in/guest-list": "POST",
|
|
10
|
+
// },
|
|
11
|
+
} satisfies BetterAuthClientPlugin
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { BetterAuthPlugin } from "better-auth"
|
|
2
|
+
import { APIError, createAuthEndpoint } from "better-auth/api"
|
|
3
|
+
import { setSessionCookie } from "better-auth/cookies"
|
|
4
|
+
import { capitalize } from "lodash-es"
|
|
5
|
+
import { z } from "zod/v4-mini"
|
|
6
|
+
import { getOrigin } from "./utils"
|
|
7
|
+
|
|
8
|
+
type GuestWithRole = {
|
|
9
|
+
name: string,
|
|
10
|
+
role: string //comma-separated string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GuestListOptions {
|
|
14
|
+
/**
|
|
15
|
+
* List of accepted guest names
|
|
16
|
+
*/
|
|
17
|
+
allowGuests: string[] | GuestWithRole[]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* When true returns the list of guest names via the guestList.reveal() endpoint and via errors.
|
|
21
|
+
* When false returns nothing.
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
revealNames?: boolean
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configure the domain name of the temporary email
|
|
28
|
+
* address for the guest users in the database.
|
|
29
|
+
* @default "baseURL"
|
|
30
|
+
*/
|
|
31
|
+
emailDomainName?: string
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Custom schema for the anonymous plugin
|
|
35
|
+
*/
|
|
36
|
+
// schema?: InferOptionSchema<typeof schema>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatName(name: string) {
|
|
40
|
+
return capitalize(name.replaceAll(/\s/g, ""))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const guestList = (options?: GuestListOptions) => {
|
|
44
|
+
const ERROR_CODES = {
|
|
45
|
+
NAME_NOT_PROVIDED: "Guest name not provided",
|
|
46
|
+
NAME_NOT_ON_GUEST_LIST: "Your name is not on the guest list",
|
|
47
|
+
NAME_ONE_WORD_ONLY: "Please only use 1-word names",
|
|
48
|
+
FAILED_TO_CREATE_USER: "Failed to create user",
|
|
49
|
+
COULD_NOT_CREATE_SESSION: "Could not create session",
|
|
50
|
+
} as const
|
|
51
|
+
|
|
52
|
+
const guestLookup = Object.fromEntries(
|
|
53
|
+
(options?.allowGuests ?? [])
|
|
54
|
+
.map((entry) => typeof entry === "string" ? { name: entry, role: "" } : entry)
|
|
55
|
+
.filter(entry => !!entry && entry.name)
|
|
56
|
+
.map(({ name, role }) => [formatName(name), {
|
|
57
|
+
name: formatName(name),
|
|
58
|
+
role: (role ?? "").split(",").map(s => s.trim()).join(",")
|
|
59
|
+
}])
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
id: "guest-list",
|
|
64
|
+
endpoints: {
|
|
65
|
+
signInGuest: createAuthEndpoint(
|
|
66
|
+
"/sign-in/guest-list",
|
|
67
|
+
{
|
|
68
|
+
method: "POST",
|
|
69
|
+
body: z.object({
|
|
70
|
+
name: z.string(),
|
|
71
|
+
}),
|
|
72
|
+
metadata: {
|
|
73
|
+
openapi: {
|
|
74
|
+
description: "Sign in as a guest with name only",
|
|
75
|
+
responses: {
|
|
76
|
+
200: {
|
|
77
|
+
description: "Sign in as a guest successful",
|
|
78
|
+
content: {
|
|
79
|
+
"application/json": {
|
|
80
|
+
schema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
user: {
|
|
84
|
+
$ref: "#/components/schemas/User",
|
|
85
|
+
},
|
|
86
|
+
session: {
|
|
87
|
+
$ref: "#/components/schemas/Session",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
async (ctx) => {
|
|
99
|
+
const { name } = ctx.body
|
|
100
|
+
|
|
101
|
+
if (!name) {
|
|
102
|
+
ctx.context.logger.error("Guest name not provided")
|
|
103
|
+
throw new APIError("UNAUTHORIZED", {
|
|
104
|
+
message: options?.revealNames
|
|
105
|
+
? `Guest name not provided. Try: ${JSON.stringify(Object.keys(guestLookup))}` : ERROR_CODES.NAME_NOT_PROVIDED,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (name.trim().split(/\s+/).length > 1) {
|
|
110
|
+
ctx.context.logger.error("For simplicity, only one word names are allowed")
|
|
111
|
+
throw new APIError("UNAUTHORIZED", {
|
|
112
|
+
message: ERROR_CODES.NAME_ONE_WORD_ONLY,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const cleanedName = formatName(name)
|
|
117
|
+
|
|
118
|
+
if (!guestLookup[cleanedName]) {
|
|
119
|
+
throw new APIError("UNAUTHORIZED", {
|
|
120
|
+
message: options?.revealNames
|
|
121
|
+
? `Name not on list. Try: ${JSON.stringify(Object.keys(guestLookup))}`
|
|
122
|
+
: ERROR_CODES.NAME_NOT_ON_GUEST_LIST,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// generate email based the input name
|
|
127
|
+
const { emailDomainName = getOrigin(ctx.context.baseURL) } = options ?? {}
|
|
128
|
+
const email = `${cleanedName.toLowerCase().replaceAll(/\s/g, "")}.onguestlist@${emailDomainName}`
|
|
129
|
+
|
|
130
|
+
const found = await ctx.context.internalAdapter.findUserByEmail(email)
|
|
131
|
+
|
|
132
|
+
async function createNewUser() {
|
|
133
|
+
const newUser = await ctx.context.internalAdapter.createUser(
|
|
134
|
+
{
|
|
135
|
+
email,
|
|
136
|
+
emailVerified: false,
|
|
137
|
+
name: cleanedName,
|
|
138
|
+
role: guestLookup[cleanedName].role,
|
|
139
|
+
createdAt: new Date(),
|
|
140
|
+
updatedAt: new Date(),
|
|
141
|
+
},
|
|
142
|
+
ctx
|
|
143
|
+
)
|
|
144
|
+
if (!newUser) {
|
|
145
|
+
throw ctx.error("INTERNAL_SERVER_ERROR", {
|
|
146
|
+
message: ERROR_CODES.FAILED_TO_CREATE_USER,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return newUser
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const user = found ? found.user : await createNewUser()
|
|
154
|
+
|
|
155
|
+
const session = await ctx.context.internalAdapter.createSession(user.id, ctx, true)
|
|
156
|
+
|
|
157
|
+
if (!session) {
|
|
158
|
+
return ctx.json(null, {
|
|
159
|
+
status: 400,
|
|
160
|
+
body: {
|
|
161
|
+
message: ERROR_CODES.COULD_NOT_CREATE_SESSION,
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
await setSessionCookie(ctx, { session, user })
|
|
166
|
+
|
|
167
|
+
return ctx.json({ token: session.token, user })
|
|
168
|
+
}
|
|
169
|
+
),
|
|
170
|
+
|
|
171
|
+
revealGuestList: createAuthEndpoint(
|
|
172
|
+
"/sign-in/guest-list/reveal",
|
|
173
|
+
{
|
|
174
|
+
method: "GET",
|
|
175
|
+
metadata: {
|
|
176
|
+
openapi: {
|
|
177
|
+
description: "Reveal guest list if 'revealNames' is enabled. Empty array otherwise",
|
|
178
|
+
responses: {
|
|
179
|
+
200: {
|
|
180
|
+
description: "List of allowed guest names or empty array",
|
|
181
|
+
content: {
|
|
182
|
+
"application/json": {
|
|
183
|
+
schema: {
|
|
184
|
+
type: "array",
|
|
185
|
+
items: {
|
|
186
|
+
type: "string",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
async (ctx) => {
|
|
197
|
+
return ctx.json(options?.revealNames ? Object.keys(guestLookup) : [])
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
//schema: mergeSchema(schema, options?.schema),
|
|
203
|
+
$ERROR_CODES: ERROR_CODES,
|
|
204
|
+
} satisfies BetterAuthPlugin
|
|
205
|
+
}
|
package/src/utils.ts
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Options, defineConfig } from "tsup"
|
|
2
|
+
|
|
3
|
+
const libCfg = {
|
|
4
|
+
entry: ["src/index.ts", "src/client.ts"],
|
|
5
|
+
splitting: false,
|
|
6
|
+
target: "node22",
|
|
7
|
+
format: ["esm", "cjs"],
|
|
8
|
+
dts: true,
|
|
9
|
+
clean: true,
|
|
10
|
+
outExtension({ format }) {
|
|
11
|
+
return { js: format === "esm" ? ".mjs" : format === "cjs" ? ".cjs" : ".js" }
|
|
12
|
+
},
|
|
13
|
+
} satisfies Options
|
|
14
|
+
|
|
15
|
+
export default defineConfig(libCfg)
|