@protobase/server 0.5.0 → 0.7.0
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/better-auth/auth-schema.d.ts +16 -0
- package/dist/better-auth/auth-schema.js +45 -0
- package/dist/better-auth/create-auth.d.ts +189 -19
- package/dist/better-auth/create-auth.js +38 -9
- package/dist/better-auth/platform-sign-in.d.ts +30 -0
- package/dist/better-auth/platform-sign-in.js +88 -0
- package/dist/better-auth/policy-plugin.js +10 -5
- package/dist/better-auth/policy-store.js +44 -30
- package/dist/better-auth/sign-in-enforcement.js +11 -3
- package/dist/better-auth/sign-in-policy.d.ts +8 -1
- package/dist/better-auth/sign-in-policy.js +21 -7
- package/dist/better-auth/snake-case-migration.js +122 -0
- package/dist/better-auth/snake-case-names.js +72 -0
- package/dist/better-auth/staff-log.js +56 -45
- package/dist/better-auth/staff-plugin.js +1 -1
- package/dist/better-auth/status-route.js +10 -4
- package/dist/better-auth/users.d.ts +8 -1
- package/dist/better-auth/users.js +16 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/package.json +4 -4
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { decodeJwt } from "jose";
|
|
2
|
+
//#region src/better-auth/platform-sign-in.ts
|
|
3
|
+
const signInIssuerVariable = "PROTOBASE_SIGN_IN_ISSUER";
|
|
4
|
+
const signInClientIdVariable = "PROTOBASE_SIGN_IN_CLIENT_ID";
|
|
5
|
+
const signInClientSecretVariable = "PROTOBASE_SIGN_IN_CLIENT_SECRET";
|
|
6
|
+
const signInProviderVariable = "PROTOBASE_SIGN_IN_PROVIDER";
|
|
7
|
+
const signInNameVariable = "PROTOBASE_SIGN_IN_NAME";
|
|
8
|
+
/**
|
|
9
|
+
* The provider the platform passes in `PROTOBASE_SIGN_IN_ISSUER`, `PROTOBASE_SIGN_IN_CLIENT_ID` and
|
|
10
|
+
* `PROTOBASE_SIGN_IN_CLIENT_SECRET` (with `PROTOBASE_SIGN_IN_PROVIDER` and `PROTOBASE_SIGN_IN_NAME` optional);
|
|
11
|
+
* `undefined` when none of the three is set. Some but not all of them stops the server at startup.
|
|
12
|
+
*/
|
|
13
|
+
const readPlatformSignIn = (env) => {
|
|
14
|
+
const required = [
|
|
15
|
+
signInIssuerVariable,
|
|
16
|
+
signInClientIdVariable,
|
|
17
|
+
signInClientSecretVariable
|
|
18
|
+
];
|
|
19
|
+
const [issuer, clientId, clientSecret] = required.map((name) => env[name]?.trim() || void 0);
|
|
20
|
+
if (!issuer && !clientId && !clientSecret) return void 0;
|
|
21
|
+
if (!issuer || !clientId || !clientSecret) throw new Error(`${required.join(", ")} go together: set all three for the platform sign-in provider, or none`);
|
|
22
|
+
const provider = env[signInProviderVariable]?.trim();
|
|
23
|
+
const name = env[signInNameVariable]?.trim();
|
|
24
|
+
return {
|
|
25
|
+
issuer,
|
|
26
|
+
clientId,
|
|
27
|
+
clientSecret,
|
|
28
|
+
...provider && { provider },
|
|
29
|
+
...name && { name }
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const providerId = /^[a-z][a-z0-9-]*$/;
|
|
33
|
+
/** The provider with its defaults filled in; an issuer that is not a URL or a malformed id is an error. */
|
|
34
|
+
const resolvePlatformSignIn = (signIn) => {
|
|
35
|
+
if (!URL.canParse(signIn.issuer)) throw new Error(`The platform sign-in provider's issuer must be a URL, not "${signIn.issuer}"`);
|
|
36
|
+
const provider = signIn.provider ?? "oidc";
|
|
37
|
+
if (!providerId.test(provider)) throw new Error(`Invalid sign-in provider id "${provider}": use lowercase letters, digits or "-"`);
|
|
38
|
+
return {
|
|
39
|
+
...signIn,
|
|
40
|
+
provider,
|
|
41
|
+
name: signIn.name ?? (provider === "github" ? "GitHub" : "SSO"),
|
|
42
|
+
scopes: signIn.scopes ?? []
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** The platform provider, unless the app configures a provider of its own with the same id, which then wins. */
|
|
46
|
+
const platformSignInFor = (signIn, socialProviders) => signIn && !socialProviders.includes(signIn.provider) ? signIn : void 0;
|
|
47
|
+
/**
|
|
48
|
+
* The person as an ID token Better Auth already verified (the provider's keys, this client and the nonce) says, once
|
|
49
|
+
* its issuer is the configured one; `null` without an ID token, so nothing comes from an unverified source.
|
|
50
|
+
*/
|
|
51
|
+
const idTokenUser = (idToken, issuer) => {
|
|
52
|
+
if (!idToken) return null;
|
|
53
|
+
const claims = decodeJwt(idToken);
|
|
54
|
+
if (claims.iss !== issuer || typeof claims.sub !== "string" || !claims.sub || typeof claims.email !== "string") return null;
|
|
55
|
+
return {
|
|
56
|
+
id: claims.sub,
|
|
57
|
+
sub: claims.sub,
|
|
58
|
+
email: claims.email,
|
|
59
|
+
emailVerified: claims.email_verified === true,
|
|
60
|
+
name: typeof claims.name === "string" && claims.name ? claims.name : claims.email.split("@")[0],
|
|
61
|
+
...typeof claims.picture === "string" && { image: claims.picture }
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The provider for Better Auth's generic OAuth plugin: discovery, PKCE, the client secret in a Basic header, an ID
|
|
66
|
+
* token that must verify against the provider's keys, and no sign-up. A provider whose settings cannot be read at
|
|
67
|
+
* startup is left out until the next start.
|
|
68
|
+
*/
|
|
69
|
+
const platformSignInProvider = (signIn) => ({
|
|
70
|
+
providerId: signIn.provider,
|
|
71
|
+
name: signIn.name,
|
|
72
|
+
clientId: signIn.clientId,
|
|
73
|
+
clientSecret: signIn.clientSecret,
|
|
74
|
+
discoveryUrl: `${signIn.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
75
|
+
scopes: [
|
|
76
|
+
"openid",
|
|
77
|
+
"email",
|
|
78
|
+
"profile",
|
|
79
|
+
...signIn.scopes
|
|
80
|
+
],
|
|
81
|
+
pkce: true,
|
|
82
|
+
requireIdTokenVerification: true,
|
|
83
|
+
authentication: "basic",
|
|
84
|
+
disableSignUp: true,
|
|
85
|
+
getUserInfo: async (tokens) => idTokenUser(tokens.idToken, signIn.issuer)
|
|
86
|
+
});
|
|
87
|
+
//#endregion
|
|
88
|
+
export { idTokenUser, platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn, signInClientIdVariable, signInClientSecretVariable, signInIssuerVariable, signInNameVariable, signInProviderVariable };
|
|
@@ -36,8 +36,9 @@ const refuse = (status, refusal) => new APIError(status, refusal);
|
|
|
36
36
|
* applies it to Better Auth's own endpoints. A method the policy turns off answers `403 SIGN_IN_METHOD_FORBIDDEN`;
|
|
37
37
|
* someone without what it requires gets a session, so they can set it up, but no API token (`403 SIGN_IN_SETUP_REQUIRED`).
|
|
38
38
|
*/
|
|
39
|
-
const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
39
|
+
const signInPolicyPlugin = ({ mail, socialProviders, operator, platformSignIn }) => {
|
|
40
40
|
const currentPolicy = async (context) => effectiveSignInPolicy((await readSignInPolicy(context.adapter)).policy, { mail });
|
|
41
|
+
const providersUnder = (policy) => [...socialProviders, ...platformSignIn && policy.platformSignIn === "allowed" ? [platformSignIn.provider] : []];
|
|
41
42
|
const policyAdmin = adminOnly("Only an admin can change how people sign in.");
|
|
42
43
|
const policyAnswer = async (context) => {
|
|
43
44
|
const stored = await readSignInPolicy(context.adapter);
|
|
@@ -47,6 +48,7 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
|
47
48
|
effective: effectiveSignInPolicy(stored.policy, { mail }),
|
|
48
49
|
mail,
|
|
49
50
|
...operator && { operator },
|
|
51
|
+
...platformSignIn && { platformSignIn: platformSignIn.name },
|
|
50
52
|
...stored.savedAt && { savedAt: stored.savedAt },
|
|
51
53
|
...savedBy && { savedBy }
|
|
52
54
|
};
|
|
@@ -72,9 +74,10 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
|
72
74
|
message: "Choose one of the offered rules for every sign-in method."
|
|
73
75
|
});
|
|
74
76
|
const user = ctx.context.session.user;
|
|
77
|
+
const admin = await accountMethods(ctx.context, user, providersUnder(effectiveSignInPolicy(policy, { mail })));
|
|
75
78
|
const problem = signInPolicyProblem(policy, {
|
|
76
79
|
mail,
|
|
77
|
-
admin
|
|
80
|
+
admin
|
|
78
81
|
});
|
|
79
82
|
if (problem) throw refuse("BAD_REQUEST", {
|
|
80
83
|
code: "SIGN_IN_POLICY_REFUSED",
|
|
@@ -92,7 +95,7 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
|
92
95
|
use: [sessionMiddleware]
|
|
93
96
|
}, async (ctx) => {
|
|
94
97
|
const policy = await currentPolicy(ctx.context);
|
|
95
|
-
const { socialSignIn: _, ...account } = await accountMethods(ctx.context, ctx.context.session.user,
|
|
98
|
+
const { socialSignIn: _, ...account } = await accountMethods(ctx.context, ctx.context.session.user, providersUnder(policy));
|
|
96
99
|
return ctx.json({
|
|
97
100
|
policy,
|
|
98
101
|
mail,
|
|
@@ -112,14 +115,16 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
|
112
115
|
const policy = await currentPolicy(ctx.context);
|
|
113
116
|
const refusal = policyRefusal(policy, {
|
|
114
117
|
path,
|
|
115
|
-
body: ctx.body
|
|
118
|
+
body: ctx.body,
|
|
119
|
+
params: ctx.params,
|
|
120
|
+
...platformSignIn && { platformProvider: platformSignIn.provider }
|
|
116
121
|
});
|
|
117
122
|
if (refusal) throw refuse("FORBIDDEN", refusal);
|
|
118
123
|
if (path === "/sign-in/email-otp") return vouchForCreatedAccount(ctx.context, ctx.body.email);
|
|
119
124
|
if (!(path === "/token" && (policy.twoFactor === "required" || policy.passkey === "required") || path === "/passkey/delete-passkey" && policy.passkey === "required")) return;
|
|
120
125
|
const session = await getSessionFromCtx(ctx);
|
|
121
126
|
if (!session || isStaffSession(session.session)) return;
|
|
122
|
-
const account = await accountMethods(ctx.context, session.user,
|
|
127
|
+
const account = await accountMethods(ctx.context, session.user, providersUnder(policy));
|
|
123
128
|
if (path === "/token" && missingRequiredMethods(policy, account).length > 0) throw refuse("FORBIDDEN", setupRequired);
|
|
124
129
|
if (path === "/passkey/delete-passkey" && account.passkeys <= 1) throw refuse("FORBIDDEN", lastPasskeyRequired);
|
|
125
130
|
})
|
|
@@ -2,37 +2,51 @@ import { defaultSignInPolicy, parseSignInPolicy } from "./sign-in-policy.js";
|
|
|
2
2
|
//#region src/better-auth/policy-store.ts
|
|
3
3
|
/** Better Auth's model for the sign-in policy: one row per save, the newest applies, the older ones are its history. */
|
|
4
4
|
const signInPolicyModel = "signInPolicy";
|
|
5
|
-
const signInPolicySchema = { [signInPolicyModel]: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
5
|
+
const signInPolicySchema = { [signInPolicyModel]: {
|
|
6
|
+
modelName: "sign_in_policy",
|
|
7
|
+
fields: {
|
|
8
|
+
password: {
|
|
9
|
+
type: "string",
|
|
10
|
+
required: true
|
|
11
|
+
},
|
|
12
|
+
emailCode: {
|
|
13
|
+
type: "string",
|
|
14
|
+
required: true,
|
|
15
|
+
fieldName: "email_code"
|
|
16
|
+
},
|
|
17
|
+
passkey: {
|
|
18
|
+
type: "string",
|
|
19
|
+
required: true
|
|
20
|
+
},
|
|
21
|
+
twoFactor: {
|
|
22
|
+
type: "string",
|
|
23
|
+
required: true,
|
|
24
|
+
fieldName: "two_factor"
|
|
25
|
+
},
|
|
26
|
+
staffAccess: {
|
|
27
|
+
type: "string",
|
|
28
|
+
required: true,
|
|
29
|
+
defaultValue: "allowed",
|
|
30
|
+
fieldName: "staff_access"
|
|
31
|
+
},
|
|
32
|
+
platformSignIn: {
|
|
33
|
+
type: "string",
|
|
34
|
+
required: true,
|
|
35
|
+
defaultValue: "allowed",
|
|
36
|
+
fieldName: "platform_sign_in"
|
|
37
|
+
},
|
|
38
|
+
createdAt: {
|
|
39
|
+
type: "date",
|
|
40
|
+
required: true,
|
|
41
|
+
fieldName: "created_at"
|
|
42
|
+
},
|
|
43
|
+
createdBy: {
|
|
44
|
+
type: "string",
|
|
45
|
+
required: false,
|
|
46
|
+
fieldName: "created_by"
|
|
47
|
+
}
|
|
34
48
|
}
|
|
35
|
-
} }
|
|
49
|
+
} };
|
|
36
50
|
/** The newest saved policy, or the default when none was saved. A row that no longer parses counts as none. */
|
|
37
51
|
const readSignInPolicy = async (adapter) => {
|
|
38
52
|
const [row] = await adapter.findMany({
|
|
@@ -13,6 +13,8 @@ const passkeyPaths = [
|
|
|
13
13
|
"/passkey/verify-registration"
|
|
14
14
|
];
|
|
15
15
|
const staffSignInPath = "/staff/sign-in";
|
|
16
|
+
const providerPaths = ["/sign-in/social", "/link-social"];
|
|
17
|
+
const callbackPath = "/callback/:id";
|
|
16
18
|
const accountPaths = [
|
|
17
19
|
"/email-otp/send-verification-otp",
|
|
18
20
|
"/passkey/delete-passkey",
|
|
@@ -26,23 +28,29 @@ const isPoliced = (path) => path !== void 0 && ([
|
|
|
26
28
|
...emailCodePaths,
|
|
27
29
|
...passkeyPaths,
|
|
28
30
|
...accountPaths,
|
|
31
|
+
...providerPaths,
|
|
32
|
+
callbackPath,
|
|
29
33
|
"/staff/sign-in"
|
|
30
34
|
].includes(path) || path.startsWith("/reset-password/"));
|
|
31
|
-
const
|
|
35
|
+
const providerOf = ({ path, body, params }) => providerPaths.includes(path) ? body?.provider : path === callbackPath ? params?.id : void 0;
|
|
36
|
+
const methodOf = (request) => {
|
|
37
|
+
const { path, body } = request;
|
|
32
38
|
if (passwordPaths.includes(path) || path.startsWith("/reset-password/")) return "password";
|
|
33
39
|
if (emailCodePaths.includes(path)) return "emailCode";
|
|
34
40
|
if (path === "/email-otp/send-verification-otp" && body?.type === "sign-in") return "emailCode";
|
|
35
41
|
if (passkeyPaths.includes(path)) return "passkey";
|
|
36
42
|
if (path === "/two-factor/enable") return "twoFactor";
|
|
37
43
|
if (path === "/staff/sign-in") return "staffAccess";
|
|
44
|
+
if (request.platformProvider !== void 0 && providerOf(request) === request.platformProvider) return "platformSignIn";
|
|
38
45
|
};
|
|
39
46
|
/**
|
|
40
47
|
* Why the policy refuses a request to Better Auth at `path`, or `undefined` when it lets it through: a method the
|
|
41
48
|
* policy turns off, or turning off two-factor authentication while the policy requires it. Removing the last passkey
|
|
42
49
|
* while passkeys are required depends on the account, so the plugin checks that itself.
|
|
43
50
|
*/
|
|
44
|
-
const policyRefusal = (policy,
|
|
45
|
-
const
|
|
51
|
+
const policyRefusal = (policy, request) => {
|
|
52
|
+
const { path } = request;
|
|
53
|
+
const method = methodOf(request);
|
|
46
54
|
if (method && policy[method] === "forbidden") return {
|
|
47
55
|
code: "SIGN_IN_METHOD_FORBIDDEN",
|
|
48
56
|
message: signInPolicyMethods[method].forbidden
|
|
@@ -7,7 +7,8 @@ export type StaffAccessRule = 'allowed' | 'notify' | 'forbidden';
|
|
|
7
7
|
* The app's sign-in policy. `password`, `emailCode` and `passkey` are ways to sign in; `twoFactor` is the second step
|
|
8
8
|
* after a password or an emailed code. Only a passkey and two-factor authentication can be required: they are set up
|
|
9
9
|
* once per person, so "required" sends everyone without one to set it up. `staffAccess` is whether the operator's
|
|
10
|
-
* staff may sign in as people, for support.
|
|
10
|
+
* staff may sign in as people, for support. `platformSignIn` is whether people may sign in through the provider the
|
|
11
|
+
* platform adds (see `PlatformSignIn`), which the app's developer did not choose.
|
|
11
12
|
*/
|
|
12
13
|
export type SignInPolicy = {
|
|
13
14
|
password: 'allowed' | 'forbidden';
|
|
@@ -15,6 +16,7 @@ export type SignInPolicy = {
|
|
|
15
16
|
passkey: SignInRule;
|
|
16
17
|
twoFactor: SignInRule;
|
|
17
18
|
staffAccess: StaffAccessRule;
|
|
19
|
+
platformSignIn: 'allowed' | 'forbidden';
|
|
18
20
|
};
|
|
19
21
|
/** Each method with the rules it takes, its name, and what the server answers when the policy turns it off. */
|
|
20
22
|
export declare const signInPolicyMethods: {
|
|
@@ -43,6 +45,11 @@ export declare const signInPolicyMethods: {
|
|
|
43
45
|
readonly rules: readonly ["allowed", "notify", "forbidden"];
|
|
44
46
|
readonly forbidden: "Staff sign-in is turned off for this app.";
|
|
45
47
|
};
|
|
48
|
+
readonly platformSignIn: {
|
|
49
|
+
readonly label: "Sign-in through the platform";
|
|
50
|
+
readonly rules: readonly ["allowed", "forbidden"];
|
|
51
|
+
readonly forbidden: "Signing in through the platform is turned off for this app.";
|
|
52
|
+
};
|
|
46
53
|
};
|
|
47
54
|
/** Everything allowed and nothing required: the policy of an app whose admins never saved one. */
|
|
48
55
|
export declare const defaultSignInPolicy: SignInPolicy;
|
|
@@ -37,6 +37,11 @@ const signInPolicyMethods = {
|
|
|
37
37
|
"forbidden"
|
|
38
38
|
],
|
|
39
39
|
forbidden: "Staff sign-in is turned off for this app."
|
|
40
|
+
},
|
|
41
|
+
platformSignIn: {
|
|
42
|
+
label: "Sign-in through the platform",
|
|
43
|
+
rules: ["allowed", "forbidden"],
|
|
44
|
+
forbidden: "Signing in through the platform is turned off for this app."
|
|
40
45
|
}
|
|
41
46
|
};
|
|
42
47
|
/** Everything allowed and nothing required: the policy of an app whose admins never saved one. */
|
|
@@ -45,7 +50,8 @@ const defaultSignInPolicy = {
|
|
|
45
50
|
emailCode: "allowed",
|
|
46
51
|
passkey: "allowed",
|
|
47
52
|
twoFactor: "allowed",
|
|
48
|
-
staffAccess: "allowed"
|
|
53
|
+
staffAccess: "allowed",
|
|
54
|
+
platformSignIn: "allowed"
|
|
49
55
|
};
|
|
50
56
|
const methodNames = Object.keys(signInPolicyMethods);
|
|
51
57
|
/** A policy from a request body or a stored row; `undefined` when a method is missing or has a rule it does not take. */
|
|
@@ -57,13 +63,21 @@ const parseSignInPolicy = (input) => {
|
|
|
57
63
|
/**
|
|
58
64
|
* The policy as it applies. Emailed codes need mail; without mail, password sign-in is on whatever the policy says, so
|
|
59
65
|
* an app whose mail settings went missing does not lock out everyone who had only emailed codes left. Staff sign-in
|
|
60
|
-
* that emails the person is off without mail, since nobody would be told.
|
|
66
|
+
* that emails the person is off without mail, since nobody would be told. Sign-in through the platform is off while
|
|
67
|
+
* passkeys or two-factor authentication are required: it skips the app's second step, and the provider's own cannot
|
|
68
|
+
* be checked.
|
|
61
69
|
*/
|
|
62
|
-
const effectiveSignInPolicy = (policy, { mail }) =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
const effectiveSignInPolicy = (policy, { mail }) => {
|
|
71
|
+
const strict = policy.passkey === "required" || policy.twoFactor === "required";
|
|
72
|
+
return {
|
|
73
|
+
...policy,
|
|
74
|
+
...!mail && {
|
|
75
|
+
password: "allowed",
|
|
76
|
+
emailCode: "forbidden"
|
|
77
|
+
},
|
|
78
|
+
...!mail && policy.staffAccess === "notify" && { staffAccess: "forbidden" },
|
|
79
|
+
...strict && { platformSignIn: "forbidden" }
|
|
80
|
+
};
|
|
67
81
|
};
|
|
68
82
|
/** The required methods `account` has not set up yet, in the order a setup page asks for them. */
|
|
69
83
|
const missingRequiredMethods = (policy, account) => [...policy.twoFactor === "required" && !account.twoFactor ? ["twoFactor"] : [], ...policy.passkey === "required" && account.passkeys === 0 ? ["passkey"] : []];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//#region src/better-auth/snake-case-migration.ts
|
|
2
|
+
const schemaName = /^[a-z_][a-z0-9_]*$/;
|
|
3
|
+
/**
|
|
4
|
+
* The migration that renames the auth store of Protobase 0.6 in place, from Better Auth's camelCase names to the
|
|
5
|
+
* snake_case ones of `snake-case-names.ts`: tables, columns, and the indexes and constraints named after them. Data,
|
|
6
|
+
* defaults, keys and indexes stay as they are. On a store that has the snake_case names already it does nothing, so a
|
|
7
|
+
* project's migrations can carry it after an auth migration written with them. `schema` is the store's schema, the
|
|
8
|
+
* connection's current one when left out.
|
|
9
|
+
*/
|
|
10
|
+
const snakeCaseMigration = (schema) => {
|
|
11
|
+
if (schema !== void 0 && !schemaName.test(schema)) throw new Error(`Invalid auth schema name "${schema}"`);
|
|
12
|
+
const s = schema === void 0 ? "" : `"${schema}".`;
|
|
13
|
+
const current = schema === void 0 ? "current_schema()" : `'${schema}'`;
|
|
14
|
+
return `-- Renames the auth store of Protobase 0.6 to snake_case. A store set up with the snake_case names has no "emailVerified"
|
|
15
|
+
-- and is left as it is.
|
|
16
|
+
do $$
|
|
17
|
+
declare
|
|
18
|
+
constraint_row record;
|
|
19
|
+
begin
|
|
20
|
+
if not exists (select from information_schema.columns where table_schema = ${current} and table_name = 'user' and column_name = 'emailVerified') then
|
|
21
|
+
return;
|
|
22
|
+
end if;
|
|
23
|
+
|
|
24
|
+
alter table ${s}"twoFactor" rename to two_factor;
|
|
25
|
+
alter table ${s}"signInPolicy" rename to sign_in_policy;
|
|
26
|
+
alter table ${s}"staffSignIn" rename to staff_sign_in;
|
|
27
|
+
|
|
28
|
+
alter table ${s}"user" rename column "emailVerified" to email_verified;
|
|
29
|
+
alter table ${s}"user" rename column "createdAt" to created_at;
|
|
30
|
+
alter table ${s}"user" rename column "updatedAt" to updated_at;
|
|
31
|
+
alter table ${s}"user" rename column "banReason" to ban_reason;
|
|
32
|
+
alter table ${s}"user" rename column "banExpires" to ban_expires;
|
|
33
|
+
alter table ${s}"user" rename column "twoFactorEnabled" to two_factor_enabled;
|
|
34
|
+
|
|
35
|
+
alter table ${s}session rename column "expiresAt" to expires_at;
|
|
36
|
+
alter table ${s}session rename column "createdAt" to created_at;
|
|
37
|
+
alter table ${s}session rename column "updatedAt" to updated_at;
|
|
38
|
+
alter table ${s}session rename column "ipAddress" to ip_address;
|
|
39
|
+
alter table ${s}session rename column "userAgent" to user_agent;
|
|
40
|
+
alter table ${s}session rename column "userId" to user_id;
|
|
41
|
+
alter table ${s}session rename column "impersonatedBy" to impersonated_by;
|
|
42
|
+
|
|
43
|
+
alter table ${s}account rename column "accountId" to account_id;
|
|
44
|
+
alter table ${s}account rename column "providerId" to provider_id;
|
|
45
|
+
alter table ${s}account rename column "userId" to user_id;
|
|
46
|
+
alter table ${s}account rename column "accessToken" to access_token;
|
|
47
|
+
alter table ${s}account rename column "refreshToken" to refresh_token;
|
|
48
|
+
alter table ${s}account rename column "idToken" to id_token;
|
|
49
|
+
alter table ${s}account rename column "accessTokenExpiresAt" to access_token_expires_at;
|
|
50
|
+
alter table ${s}account rename column "refreshTokenExpiresAt" to refresh_token_expires_at;
|
|
51
|
+
alter table ${s}account rename column "createdAt" to created_at;
|
|
52
|
+
alter table ${s}account rename column "updatedAt" to updated_at;
|
|
53
|
+
|
|
54
|
+
alter table ${s}verification rename column "expiresAt" to expires_at;
|
|
55
|
+
alter table ${s}verification rename column "createdAt" to created_at;
|
|
56
|
+
alter table ${s}verification rename column "updatedAt" to updated_at;
|
|
57
|
+
|
|
58
|
+
alter table ${s}jwks rename column "publicKey" to public_key;
|
|
59
|
+
alter table ${s}jwks rename column "privateKey" to private_key;
|
|
60
|
+
alter table ${s}jwks rename column "createdAt" to created_at;
|
|
61
|
+
alter table ${s}jwks rename column "expiresAt" to expires_at;
|
|
62
|
+
|
|
63
|
+
alter table ${s}two_factor rename column "backupCodes" to backup_codes;
|
|
64
|
+
alter table ${s}two_factor rename column "userId" to user_id;
|
|
65
|
+
alter table ${s}two_factor rename column "failedVerificationCount" to failed_verification_count;
|
|
66
|
+
alter table ${s}two_factor rename column "lockedUntil" to locked_until;
|
|
67
|
+
|
|
68
|
+
alter table ${s}passkey rename column "publicKey" to public_key;
|
|
69
|
+
alter table ${s}passkey rename column "userId" to user_id;
|
|
70
|
+
alter table ${s}passkey rename column "credentialID" to credential_id;
|
|
71
|
+
alter table ${s}passkey rename column "deviceType" to device_type;
|
|
72
|
+
alter table ${s}passkey rename column "backedUp" to backed_up;
|
|
73
|
+
alter table ${s}passkey rename column "createdAt" to created_at;
|
|
74
|
+
|
|
75
|
+
alter table ${s}sign_in_policy rename column "emailCode" to email_code;
|
|
76
|
+
alter table ${s}sign_in_policy rename column "twoFactor" to two_factor;
|
|
77
|
+
alter table ${s}sign_in_policy rename column "staffAccess" to staff_access;
|
|
78
|
+
alter table ${s}sign_in_policy rename column "platformSignIn" to platform_sign_in;
|
|
79
|
+
alter table ${s}sign_in_policy rename column "createdAt" to created_at;
|
|
80
|
+
alter table ${s}sign_in_policy rename column "createdBy" to created_by;
|
|
81
|
+
|
|
82
|
+
alter table ${s}staff_sign_in rename column "userId" to user_id;
|
|
83
|
+
alter table ${s}staff_sign_in rename column "userEmail" to user_email;
|
|
84
|
+
alter table ${s}staff_sign_in rename column "staffName" to staff_name;
|
|
85
|
+
alter table ${s}staff_sign_in rename column "staffSubject" to staff_subject;
|
|
86
|
+
alter table ${s}staff_sign_in rename column "sessionId" to session_id;
|
|
87
|
+
alter table ${s}staff_sign_in rename column "startedAt" to started_at;
|
|
88
|
+
alter table ${s}staff_sign_in rename column "expiresAt" to expires_at;
|
|
89
|
+
alter table ${s}staff_sign_in rename column "endedAt" to ended_at;
|
|
90
|
+
|
|
91
|
+
-- A primary key's index takes the new name with it.
|
|
92
|
+
alter table ${s}two_factor rename constraint "twoFactor_pkey" to two_factor_pkey;
|
|
93
|
+
alter table ${s}sign_in_policy rename constraint "signInPolicy_pkey" to sign_in_policy_pkey;
|
|
94
|
+
alter table ${s}staff_sign_in rename constraint "staffSignIn_pkey" to staff_sign_in_pkey;
|
|
95
|
+
alter table ${s}session rename constraint "session_userId_fkey" to session_user_id_fkey;
|
|
96
|
+
alter table ${s}account rename constraint "account_userId_fkey" to account_user_id_fkey;
|
|
97
|
+
alter table ${s}two_factor rename constraint "twoFactor_userId_fkey" to two_factor_user_id_fkey;
|
|
98
|
+
alter table ${s}passkey rename constraint "passkey_userId_fkey" to passkey_user_id_fkey;
|
|
99
|
+
|
|
100
|
+
alter index ${s}"session_userId_idx" rename to session_user_id_idx;
|
|
101
|
+
alter index ${s}"account_userId_idx" rename to account_user_id_idx;
|
|
102
|
+
alter index ${s}"twoFactor_secret_idx" rename to two_factor_secret_idx;
|
|
103
|
+
alter index ${s}"twoFactor_userId_idx" rename to two_factor_user_id_idx;
|
|
104
|
+
alter index ${s}"passkey_userId_idx" rename to passkey_user_id_idx;
|
|
105
|
+
alter index ${s}"passkey_credentialID_idx" rename to passkey_credential_id_idx;
|
|
106
|
+
|
|
107
|
+
-- Postgres 18 names each not-null constraint after its table and column, as "user_emailVerified_not_null"; earlier
|
|
108
|
+
-- versions keep no such constraints, so there is nothing to rename there.
|
|
109
|
+
for constraint_row in
|
|
110
|
+
select c.conrelid::regclass as table_name, c.conname as old_name, t.relname || '_' || a.attname || '_not_null' as new_name
|
|
111
|
+
from pg_constraint c
|
|
112
|
+
join pg_class t on t.oid = c.conrelid
|
|
113
|
+
join pg_attribute a on a.attrelid = c.conrelid and a.attnum = c.conkey[1]
|
|
114
|
+
where c.contype = 'n' and t.relnamespace = (select oid from pg_namespace where nspname = ${current}) and c.conname ~ '[A-Z]'
|
|
115
|
+
loop
|
|
116
|
+
execute format('alter table %s rename constraint %I to %I', constraint_row.table_name, constraint_row.old_name, constraint_row.new_name);
|
|
117
|
+
end loop;
|
|
118
|
+
end
|
|
119
|
+
$$;`;
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
export { snakeCaseMigration };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/better-auth/snake-case-names.ts
|
|
2
|
+
/** Better Auth's own tables, as its `user`, `session`, `account` and `verification` options name them. */
|
|
3
|
+
const coreNames = {
|
|
4
|
+
user: { fields: {
|
|
5
|
+
emailVerified: "email_verified",
|
|
6
|
+
createdAt: "created_at",
|
|
7
|
+
updatedAt: "updated_at"
|
|
8
|
+
} },
|
|
9
|
+
session: { fields: {
|
|
10
|
+
expiresAt: "expires_at",
|
|
11
|
+
createdAt: "created_at",
|
|
12
|
+
updatedAt: "updated_at",
|
|
13
|
+
ipAddress: "ip_address",
|
|
14
|
+
userAgent: "user_agent",
|
|
15
|
+
userId: "user_id"
|
|
16
|
+
} },
|
|
17
|
+
account: { fields: {
|
|
18
|
+
accountId: "account_id",
|
|
19
|
+
providerId: "provider_id",
|
|
20
|
+
userId: "user_id",
|
|
21
|
+
accessToken: "access_token",
|
|
22
|
+
refreshToken: "refresh_token",
|
|
23
|
+
idToken: "id_token",
|
|
24
|
+
accessTokenExpiresAt: "access_token_expires_at",
|
|
25
|
+
refreshTokenExpiresAt: "refresh_token_expires_at",
|
|
26
|
+
createdAt: "created_at",
|
|
27
|
+
updatedAt: "updated_at"
|
|
28
|
+
} },
|
|
29
|
+
verification: { fields: {
|
|
30
|
+
expiresAt: "expires_at",
|
|
31
|
+
createdAt: "created_at",
|
|
32
|
+
updatedAt: "updated_at"
|
|
33
|
+
} }
|
|
34
|
+
};
|
|
35
|
+
/** The `schema` option of each plugin with tables or columns of its own. */
|
|
36
|
+
const pluginNames = {
|
|
37
|
+
admin: {
|
|
38
|
+
user: { fields: {
|
|
39
|
+
banReason: "ban_reason",
|
|
40
|
+
banExpires: "ban_expires"
|
|
41
|
+
} },
|
|
42
|
+
session: { fields: { impersonatedBy: "impersonated_by" } }
|
|
43
|
+
},
|
|
44
|
+
jwt: { jwks: { fields: {
|
|
45
|
+
publicKey: "public_key",
|
|
46
|
+
privateKey: "private_key",
|
|
47
|
+
createdAt: "created_at",
|
|
48
|
+
expiresAt: "expires_at"
|
|
49
|
+
} } },
|
|
50
|
+
twoFactor: {
|
|
51
|
+
user: { fields: { twoFactorEnabled: "two_factor_enabled" } },
|
|
52
|
+
twoFactor: {
|
|
53
|
+
modelName: "two_factor",
|
|
54
|
+
fields: {
|
|
55
|
+
backupCodes: "backup_codes",
|
|
56
|
+
userId: "user_id",
|
|
57
|
+
failedVerificationCount: "failed_verification_count",
|
|
58
|
+
lockedUntil: "locked_until"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
passkey: { passkey: { fields: {
|
|
63
|
+
publicKey: "public_key",
|
|
64
|
+
userId: "user_id",
|
|
65
|
+
credentialID: "credential_id",
|
|
66
|
+
deviceType: "device_type",
|
|
67
|
+
backedUp: "backed_up",
|
|
68
|
+
createdAt: "created_at"
|
|
69
|
+
} } }
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { coreNames, pluginNames };
|
|
@@ -1,52 +1,63 @@
|
|
|
1
1
|
//#region src/better-auth/staff-log.ts
|
|
2
2
|
/** Better Auth's model for the log of staff sign-ins: one row per session staff started as someone. */
|
|
3
3
|
const staffSignInModel = "staffSignIn";
|
|
4
|
-
const staffSignInSchema = { [staffSignInModel]: {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
4
|
+
const staffSignInSchema = { [staffSignInModel]: {
|
|
5
|
+
modelName: "staff_sign_in",
|
|
6
|
+
fields: {
|
|
7
|
+
userId: {
|
|
8
|
+
type: "string",
|
|
9
|
+
required: true,
|
|
10
|
+
fieldName: "user_id"
|
|
11
|
+
},
|
|
12
|
+
userEmail: {
|
|
13
|
+
type: "string",
|
|
14
|
+
required: true,
|
|
15
|
+
fieldName: "user_email"
|
|
16
|
+
},
|
|
17
|
+
staff: {
|
|
18
|
+
type: "string",
|
|
19
|
+
required: true
|
|
20
|
+
},
|
|
21
|
+
staffName: {
|
|
22
|
+
type: "string",
|
|
23
|
+
required: false,
|
|
24
|
+
fieldName: "staff_name"
|
|
25
|
+
},
|
|
26
|
+
staffSubject: {
|
|
27
|
+
type: "string",
|
|
28
|
+
required: true,
|
|
29
|
+
fieldName: "staff_subject"
|
|
30
|
+
},
|
|
31
|
+
issuer: {
|
|
32
|
+
type: "string",
|
|
33
|
+
required: true
|
|
34
|
+
},
|
|
35
|
+
reason: {
|
|
36
|
+
type: "string",
|
|
37
|
+
required: true
|
|
38
|
+
},
|
|
39
|
+
sessionId: {
|
|
40
|
+
type: "string",
|
|
41
|
+
required: true,
|
|
42
|
+
fieldName: "session_id"
|
|
43
|
+
},
|
|
44
|
+
startedAt: {
|
|
45
|
+
type: "date",
|
|
46
|
+
required: true,
|
|
47
|
+
fieldName: "started_at"
|
|
48
|
+
},
|
|
49
|
+
expiresAt: {
|
|
50
|
+
type: "date",
|
|
51
|
+
required: true,
|
|
52
|
+
fieldName: "expires_at"
|
|
53
|
+
},
|
|
54
|
+
endedAt: {
|
|
55
|
+
type: "date",
|
|
56
|
+
required: false,
|
|
57
|
+
fieldName: "ended_at"
|
|
58
|
+
}
|
|
48
59
|
}
|
|
49
|
-
} }
|
|
60
|
+
} };
|
|
50
61
|
const entry = (row) => ({
|
|
51
62
|
id: row.id,
|
|
52
63
|
user: row.userEmail,
|