@everystack/auth 0.4.8 → 0.4.10
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/CHANGELOG.md +25 -0
- package/README.md +10 -0
- package/package.json +4 -4
- package/src/account.ts +44 -0
- package/src/index.ts +4 -51
- package/src/models/functions.ts +91 -2
- package/src/postgres.ts +3 -13
- package/src/sql.ts +30 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to `@everystack/auth` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.4.10] - 2026-10-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- accountTrigger ends an account's sessions in its soft-delete transaction
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- accountTrigger keeps an admin's earlier disable
|
|
16
|
+
- review nits: cap accountTrigger's column at 39 characters; docs state the column the trigger trusts
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
- accountTrigger in the lifecycle guide, the auth README, upgrading and the MCP resource
|
|
21
|
+
|
|
22
|
+
## [0.4.9] - 2026-10-14
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- break the model and auth import cycles; gate new ones
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- share refresh token extraction
|
|
31
|
+
|
|
7
32
|
## [0.4.8] - 2026-09-26
|
|
8
33
|
|
|
9
34
|
### Fixed
|
package/README.md
CHANGED
|
@@ -185,6 +185,16 @@ Edge verification trusts the signature; it cannot consult a revocation list with
|
|
|
185
185
|
- Keep **refresh tokens long-lived in DB** with revocation
|
|
186
186
|
- Sign-out / password change → revoke refresh tokens, access tokens expire on their own
|
|
187
187
|
|
|
188
|
+
## Account deletion
|
|
189
|
+
|
|
190
|
+
When an app deletes an account by soft-deleting a lifecycle row (a profile whose `id` is the
|
|
191
|
+
`auth.users` id), put `accountTrigger({ userId: 'id' })` in that model's `triggers`. In the same
|
|
192
|
+
transaction it marks the `auth.users` row deleted and removes the user's refresh and one-time tokens,
|
|
193
|
+
so sign-in and refresh stop at once. An access token already issued lives out its TTL (default 3600s).
|
|
194
|
+
Restoring the row clears the mark only: the user signs in again, and no old session comes back.
|
|
195
|
+
Identities and MFA factors are kept. It replaces an app's own token `DELETE` in its delete-account
|
|
196
|
+
code. See [docs/lifecycle.md](../../docs/lifecycle.md), "When the deleted row is an account".
|
|
197
|
+
|
|
188
198
|
## SQL Twin
|
|
189
199
|
|
|
190
200
|
`@everystack/auth/jwt/sql` ships pgcrypto-based functions that produce byte-identical tokens to the JS path:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/auth",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"description": "JWT authentication handlers, password hashing, and React auth context",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
},
|
|
109
109
|
"dependencies": {
|
|
110
110
|
"bcryptjs": "3.0.3",
|
|
111
|
-
"@everystack/model": "0.4.
|
|
111
|
+
"@everystack/model": "0.4.25"
|
|
112
112
|
},
|
|
113
113
|
"peerDependencies": {
|
|
114
114
|
"@everystack/api": ">=0.1.0",
|
|
@@ -137,8 +137,8 @@
|
|
|
137
137
|
"postgres": "3.4.9",
|
|
138
138
|
"ts-jest": "29.4.9",
|
|
139
139
|
"typescript": "5.9.3",
|
|
140
|
-
"@everystack/
|
|
141
|
-
"@everystack/
|
|
140
|
+
"@everystack/api": "0.4.13",
|
|
141
|
+
"@everystack/server": "0.4.40"
|
|
142
142
|
},
|
|
143
143
|
"scripts": {
|
|
144
144
|
"test": "jest",
|
package/src/account.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account-state and password-column helpers shared by index.ts and postgres.ts.
|
|
3
|
+
* A leaf module: index.ts imports postgres.ts, so these cannot live in index.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Why a user row may not hold a session, or null when it may. Reads row fields, not table
|
|
8
|
+
* columns, so an app schema without `bannedUntil`/`deletedAt` simply never trips it.
|
|
9
|
+
* Mirrors the SQL side: auth.sign_in filters deleted_at, auth.validate_refresh_token
|
|
10
|
+
* rejects deleted_at (P0004) and an unexpired banned_until (P0007).
|
|
11
|
+
*/
|
|
12
|
+
export function accountBlock(user: any): 'deleted' | 'banned' | null {
|
|
13
|
+
if (user.deletedAt) return 'deleted';
|
|
14
|
+
if (user.bannedUntil && new Date(user.bannedUntil).getTime() > Date.now()) return 'banned';
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The refusal for a sign-in path that already proved identity (OAuth), or null to proceed.
|
|
19
|
+
* A missing row is treated as a deleted account. */
|
|
20
|
+
export function accountBlockedResponse(user: any): Response | null {
|
|
21
|
+
const block = user ? accountBlock(user) : 'deleted';
|
|
22
|
+
if (block === 'banned') return Response.json({ message: 'Account suspended' }, { status: 403 });
|
|
23
|
+
if (block === 'deleted') return Response.json({ message: 'Account disabled' }, { status: 401 });
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Signout must always clear cookies. A failed server-side revoke is logged, never thrown:
|
|
28
|
+
* only the error's code or message is logged, never the token. */
|
|
29
|
+
export function warnRevokeFailed(err: unknown): void {
|
|
30
|
+
const e = err as { code?: string; message?: string } | undefined;
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.warn('[everystack/auth] signout: refresh token revocation failed:', e?.code ?? e?.message ?? 'unknown error');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build insert values with the correct password column name.
|
|
37
|
+
* Checks which column exists on the Drizzle table definition.
|
|
38
|
+
*/
|
|
39
|
+
export function withPassword(users: any, values: Record<string, any>, hash: string | null): Record<string, any> {
|
|
40
|
+
if ('encryptedPassword' in users) {
|
|
41
|
+
return { ...values, encryptedPassword: hash };
|
|
42
|
+
}
|
|
43
|
+
return { ...values, passwordHash: hash };
|
|
44
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -38,7 +38,9 @@ import type { OAuthProvider, OAuthUserInfo } from './oauth';
|
|
|
38
38
|
export type { OAuthProvider, OAuthUserInfo } from './oauth';
|
|
39
39
|
export { createAppleProvider, createGoogleProvider } from './oauth';
|
|
40
40
|
import { createPostgresAuthHandlers } from './postgres';
|
|
41
|
-
import {
|
|
41
|
+
import { accountBlock, accountBlockedResponse, warnRevokeFailed, withPassword } from './account';
|
|
42
|
+
export { accountBlock, accountBlockedResponse, warnRevokeFailed, withPassword } from './account';
|
|
43
|
+
import { readPresentedRefreshToken } from './cookies';
|
|
42
44
|
import { authResponse, signoutResponse } from './response';
|
|
43
45
|
import type { CookieConfig } from './response';
|
|
44
46
|
import type { EnrichClaimsFn } from './plugin';
|
|
@@ -63,45 +65,6 @@ export function getPasswordHash(user: any): string | null {
|
|
|
63
65
|
return user.encryptedPassword ?? user.passwordHash ?? null;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
/**
|
|
67
|
-
* Build insert values with the correct password column name.
|
|
68
|
-
* Checks which column exists on the Drizzle table definition.
|
|
69
|
-
*/
|
|
70
|
-
/**
|
|
71
|
-
* Why a user row may not hold a session, or null when it may. Reads row fields, not table
|
|
72
|
-
* columns, so an app schema without `bannedUntil`/`deletedAt` simply never trips it.
|
|
73
|
-
* Mirrors the SQL side: auth.sign_in filters deleted_at, auth.validate_refresh_token
|
|
74
|
-
* rejects deleted_at (P0004) and an unexpired banned_until (P0007).
|
|
75
|
-
*/
|
|
76
|
-
export function accountBlock(user: any): 'deleted' | 'banned' | null {
|
|
77
|
-
if (user.deletedAt) return 'deleted';
|
|
78
|
-
if (user.bannedUntil && new Date(user.bannedUntil).getTime() > Date.now()) return 'banned';
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** The refusal for a sign-in path that already proved identity (OAuth), or null to proceed.
|
|
83
|
-
* A missing row is treated as a deleted account. */
|
|
84
|
-
export function accountBlockedResponse(user: any): Response | null {
|
|
85
|
-
const block = user ? accountBlock(user) : 'deleted';
|
|
86
|
-
if (block === 'banned') return Response.json({ message: 'Account suspended' }, { status: 403 });
|
|
87
|
-
if (block === 'deleted') return Response.json({ message: 'Account disabled' }, { status: 401 });
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Signout must always clear cookies. A failed server-side revoke is logged, never thrown:
|
|
92
|
-
* only the error's code or message is logged, never the token. */
|
|
93
|
-
export function warnRevokeFailed(err: unknown): void {
|
|
94
|
-
const e = err as { code?: string; message?: string } | undefined;
|
|
95
|
-
// eslint-disable-next-line no-console
|
|
96
|
-
console.warn('[everystack/auth] signout: refresh token revocation failed:', e?.code ?? e?.message ?? 'unknown error');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function withPassword(users: any, values: Record<string, any>, hash: string | null): Record<string, any> {
|
|
100
|
-
if ('encryptedPassword' in users) {
|
|
101
|
-
return { ...values, encryptedPassword: hash };
|
|
102
|
-
}
|
|
103
|
-
return { ...values, passwordHash: hash };
|
|
104
|
-
}
|
|
105
68
|
|
|
106
69
|
export interface TokenPayload {
|
|
107
70
|
sub: string;
|
|
@@ -656,17 +619,7 @@ export function createAuthHandlers(
|
|
|
656
619
|
const csrfError = csrfCheck(request, options?.cookies);
|
|
657
620
|
if (csrfError) return csrfError;
|
|
658
621
|
|
|
659
|
-
|
|
660
|
-
let refreshToken: string | undefined;
|
|
661
|
-
try {
|
|
662
|
-
const body = await request.clone().json();
|
|
663
|
-
refreshToken = body?.refreshToken;
|
|
664
|
-
} catch {
|
|
665
|
-
// Empty body is fine — fall through to cookie
|
|
666
|
-
}
|
|
667
|
-
if (!refreshToken) {
|
|
668
|
-
refreshToken = getRefreshFromCookies(request.headers.get('cookie')) ?? undefined;
|
|
669
|
-
}
|
|
622
|
+
const refreshToken = await readPresentedRefreshToken(request);
|
|
670
623
|
if (!refreshToken) {
|
|
671
624
|
return Response.json(
|
|
672
625
|
{ message: 'refreshToken is required' },
|
package/src/models/functions.ts
CHANGED
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
* hand-patch a body here.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import { defineFunction, defineTrigger, arg, can, sql } from '@everystack/model';
|
|
42
|
+
import { defineFunction, defineTrigger, trigger, arg, can, sql } from '@everystack/model';
|
|
43
|
+
import type { FunctionDescriptor, RawSql, TriggerSpec } from '@everystack/model';
|
|
43
44
|
import { DEFAULT_ACCESS_TOKEN_TTL_SEC, resolveAccessTokenTtl } from '../sql';
|
|
44
45
|
import { User } from './user';
|
|
45
46
|
|
|
@@ -880,6 +881,94 @@ export const revokeRefreshTokensOnUserBlockTrigger = defineTrigger('users_revoke
|
|
|
880
881
|
execute: revokeRefreshTokensOnUserBlockFn,
|
|
881
882
|
});
|
|
882
883
|
|
|
884
|
+
/** A column name the account trigger may read: lower snake case, at most 39 characters, so
|
|
885
|
+
* `end_account_sessions_by_<col>` stays within PostgreSQL's 63-character identifier limit. */
|
|
886
|
+
const ACCOUNT_USER_ID = /^[a-z_][a-z0-9_]{0,38}$/;
|
|
887
|
+
|
|
888
|
+
/** The function name for one user-id column. `id` keeps the plain name; the auth module declares it. */
|
|
889
|
+
function accountSessionsName(userId: string): string {
|
|
890
|
+
return userId === 'id' ? 'auth.end_account_sessions' : `auth.end_account_sessions_by_${userId}`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function accountSessionsBody(userId: string): RawSql {
|
|
894
|
+
// The column is baked into the body: TriggerSpec carries no trigger arguments. It is validated
|
|
895
|
+
// as a lower snake case identifier before it gets here, so it needs no quoting.
|
|
896
|
+
return sql`
|
|
897
|
+
DECLARE
|
|
898
|
+
v_user_id pg_catalog.uuid;
|
|
899
|
+
BEGIN
|
|
900
|
+
IF TG_OP <> 'UPDATE' OR TG_WHEN <> 'AFTER' OR TG_LEVEL <> 'ROW' THEN
|
|
901
|
+
RAISE EXCEPTION 'end_account_sessions runs only as an AFTER UPDATE row trigger' USING ERRCODE = '42501';
|
|
902
|
+
END IF;
|
|
903
|
+
v_user_id := NEW.${userId}::pg_catalog.uuid;
|
|
904
|
+
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
|
905
|
+
UPDATE auth.users SET deleted_at = NEW.deleted_at WHERE id = v_user_id AND deleted_at IS NULL;
|
|
906
|
+
PERFORM auth.revoke_refresh_tokens(v_user_id);
|
|
907
|
+
DELETE FROM auth.one_time_tokens WHERE user_id = v_user_id;
|
|
908
|
+
ELSIF OLD.deleted_at IS NOT NULL AND NEW.deleted_at IS NULL THEN
|
|
909
|
+
-- A restore brings the account back, never its sessions: the user signs in again.
|
|
910
|
+
-- It clears only the stamp this account set; an earlier disable stays.
|
|
911
|
+
UPDATE auth.users SET deleted_at = NULL WHERE id = v_user_id AND deleted_at = OLD.deleted_at;
|
|
912
|
+
END IF;
|
|
913
|
+
RETURN NULL;
|
|
914
|
+
END`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const accountSessionsFunctions = new Map<string, FunctionDescriptor>();
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* The trigger function that carries an account's soft delete to auth.users, for an account table
|
|
921
|
+
* whose `userId` column holds the auth.users id. Deleting the account soft-deletes the user and
|
|
922
|
+
* removes its refresh and one-time tokens in the same transaction; restoring it clears the user's
|
|
923
|
+
* deleted_at and nothing else. Any other update writes nothing.
|
|
924
|
+
* It stamps only a live user and clears only its own stamp, so an earlier admin disable survives both.
|
|
925
|
+
*
|
|
926
|
+
* It runs inside the lifecycle functions' pinned search_path, so it pins `pg_catalog, pg_temp` and
|
|
927
|
+
* qualifies every name. No role but its owner may EXECUTE it (the compiler revokes PUBLIC).
|
|
928
|
+
*
|
|
929
|
+
* The auth module declares the `id` one. Declare any other in your own module's `derived`.
|
|
930
|
+
*/
|
|
931
|
+
export function accountSessionsFunction(userId: string): FunctionDescriptor {
|
|
932
|
+
if (!ACCOUNT_USER_ID.test(userId)) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`accountTrigger: userId '${userId}' is not a column name — pass the account table's column ` +
|
|
935
|
+
'(lower snake case, at most 39 characters), for example { userId: \'id\' } or { userId: \'user_id\' }.',
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
let fn = accountSessionsFunctions.get(userId);
|
|
939
|
+
if (!fn) {
|
|
940
|
+
fn = defineFunction(accountSessionsName(userId), {
|
|
941
|
+
returns: 'trigger',
|
|
942
|
+
language: 'plpgsql',
|
|
943
|
+
security: 'definer',
|
|
944
|
+
searchPath: ['pg_catalog', 'pg_temp'],
|
|
945
|
+
body: accountSessionsBody(userId),
|
|
946
|
+
});
|
|
947
|
+
accountSessionsFunctions.set(userId, fn);
|
|
948
|
+
}
|
|
949
|
+
return fn;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** The `id` account function, which the auth module declares. */
|
|
953
|
+
export const endAccountSessionsFn = accountSessionsFunction('id');
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Ends an account's sessions when the account is soft-deleted. Add it to the lifecycle model that
|
|
957
|
+
* IS the account (`lifecycle: true`), whose `userId` column holds the auth.users id:
|
|
958
|
+
* `triggers: [accountTrigger({ userId: 'id' })]`.
|
|
959
|
+
*
|
|
960
|
+
* AFTER UPDATE OF deleted_at, so it fires on a direct delete, a cascaded one and a restore alike.
|
|
961
|
+
*/
|
|
962
|
+
export function accountTrigger(opts: { userId: string }): TriggerSpec {
|
|
963
|
+
return trigger('end_account_sessions', {
|
|
964
|
+
timing: 'after',
|
|
965
|
+
events: ['update'],
|
|
966
|
+
of: ['deleted_at'],
|
|
967
|
+
forEach: 'row',
|
|
968
|
+
execute: accountSessionsFunction(opts.userId),
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
883
972
|
export const verifyJwtFn = defineFunction('auth.verify_jwt', {
|
|
884
973
|
args: [arg('p_token', 'text')],
|
|
885
974
|
returns: 'jsonb',
|
|
@@ -1010,7 +1099,7 @@ export const coreFunctions = [hashPasswordFn, verifyPasswordFn, signJwtFn, verif
|
|
|
1010
1099
|
/** The refresh-token family (RFC 6819 rotation). Always on. */
|
|
1011
1100
|
export const refreshTokenFunctions = [
|
|
1012
1101
|
issueRefreshTokenFn, validateRefreshTokenFn, revokeRefreshTokensFn, signOutFn, cleanupRefreshTokensFn,
|
|
1013
|
-
revokeRefreshTokensOnUserBlockFn,
|
|
1102
|
+
revokeRefreshTokensOnUserBlockFn, endAccountSessionsFn,
|
|
1014
1103
|
];
|
|
1015
1104
|
|
|
1016
1105
|
/** `auth.uid()` / `auth.role()` / `auth.jwt()` — called FROM RLS policies, so granted to the
|
package/src/postgres.ts
CHANGED
|
@@ -18,9 +18,9 @@ import { createJwt } from './jwt';
|
|
|
18
18
|
import { hmacNode } from './jwt/hmac-node';
|
|
19
19
|
import type { RefreshTokenOptions } from './refresh';
|
|
20
20
|
import type { AuthHandlers, AuthOptions, TokenPayload } from './index';
|
|
21
|
-
import { accountBlock, accountBlockedResponse, warnRevokeFailed, withPassword } from './
|
|
21
|
+
import { accountBlock, accountBlockedResponse, warnRevokeFailed, withPassword } from './account';
|
|
22
22
|
import type { OAuthProvider } from './oauth';
|
|
23
|
-
import {
|
|
23
|
+
import { readPresentedRefreshToken } from './cookies';
|
|
24
24
|
import { authResponse, signoutResponse } from './response';
|
|
25
25
|
import type { CookieConfig } from './response';
|
|
26
26
|
import { validateCsrfOrigin } from './csrf';
|
|
@@ -436,17 +436,7 @@ export function createPostgresAuthHandlers(
|
|
|
436
436
|
const csrfError = csrfCheck(request, options?.cookies);
|
|
437
437
|
if (csrfError) return csrfError;
|
|
438
438
|
|
|
439
|
-
|
|
440
|
-
let refreshToken: string | undefined;
|
|
441
|
-
try {
|
|
442
|
-
const body = await request.clone().json();
|
|
443
|
-
refreshToken = body?.refreshToken;
|
|
444
|
-
} catch {
|
|
445
|
-
// Empty body is fine — fall through to cookie
|
|
446
|
-
}
|
|
447
|
-
if (!refreshToken) {
|
|
448
|
-
refreshToken = getRefreshFromCookies(request.headers.get('cookie')) ?? undefined;
|
|
449
|
-
}
|
|
439
|
+
const refreshToken = await readPresentedRefreshToken(request);
|
|
450
440
|
if (!refreshToken) {
|
|
451
441
|
return Response.json(
|
|
452
442
|
{ message: 'refreshToken is required' },
|
package/src/sql.ts
CHANGED
|
@@ -385,6 +385,35 @@ WHEN (
|
|
|
385
385
|
)
|
|
386
386
|
EXECUTE FUNCTION auth.revoke_refresh_tokens_on_user_block();`;
|
|
387
387
|
|
|
388
|
+
const END_ACCOUNT_SESSIONS = `\
|
|
389
|
+
CREATE OR REPLACE FUNCTION auth.end_account_sessions()
|
|
390
|
+
RETURNS TRIGGER
|
|
391
|
+
LANGUAGE plpgsql
|
|
392
|
+
SECURITY DEFINER
|
|
393
|
+
SET search_path = pg_catalog, pg_temp
|
|
394
|
+
AS $$
|
|
395
|
+
DECLARE
|
|
396
|
+
v_user_id pg_catalog.uuid;
|
|
397
|
+
BEGIN
|
|
398
|
+
IF TG_OP <> 'UPDATE' OR TG_WHEN <> 'AFTER' OR TG_LEVEL <> 'ROW' THEN
|
|
399
|
+
RAISE EXCEPTION 'end_account_sessions runs only as an AFTER UPDATE row trigger' USING ERRCODE = '42501';
|
|
400
|
+
END IF;
|
|
401
|
+
v_user_id := NEW.id::pg_catalog.uuid;
|
|
402
|
+
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
|
403
|
+
UPDATE auth.users SET deleted_at = NEW.deleted_at WHERE id = v_user_id AND deleted_at IS NULL;
|
|
404
|
+
PERFORM auth.revoke_refresh_tokens(v_user_id);
|
|
405
|
+
DELETE FROM auth.one_time_tokens WHERE user_id = v_user_id;
|
|
406
|
+
ELSIF OLD.deleted_at IS NOT NULL AND NEW.deleted_at IS NULL THEN
|
|
407
|
+
-- A restore brings the account back, never its sessions: the user signs in again.
|
|
408
|
+
-- It clears only the stamp this account set; an earlier disable stays.
|
|
409
|
+
UPDATE auth.users SET deleted_at = NULL WHERE id = v_user_id AND deleted_at = OLD.deleted_at;
|
|
410
|
+
END IF;
|
|
411
|
+
RETURN NULL;
|
|
412
|
+
END
|
|
413
|
+
$$;
|
|
414
|
+
|
|
415
|
+
REVOKE EXECUTE ON FUNCTION auth.end_account_sessions FROM PUBLIC;`;
|
|
416
|
+
|
|
388
417
|
const REVOKE_REFRESH_TOKENS = `\
|
|
389
418
|
CREATE OR REPLACE FUNCTION auth.revoke_refresh_tokens(
|
|
390
419
|
p_user_id UUID,
|
|
@@ -1120,7 +1149,7 @@ export function generateCoreSQL(): string {
|
|
|
1120
1149
|
export function generateRefreshTokenSQL(): string {
|
|
1121
1150
|
return [
|
|
1122
1151
|
ISSUE_REFRESH_TOKEN, '', VALIDATE_REFRESH_TOKEN, '', REVOKE_REFRESH_TOKENS_ON_USER_BLOCK, '',
|
|
1123
|
-
REVOKE_REFRESH_TOKENS, '', SIGN_OUT, '', CLEANUP_REFRESH_TOKENS,
|
|
1152
|
+
REVOKE_REFRESH_TOKENS, '', SIGN_OUT, '', CLEANUP_REFRESH_TOKENS, '', END_ACCOUNT_SESSIONS,
|
|
1124
1153
|
]
|
|
1125
1154
|
.join('\n')
|
|
1126
1155
|
.trimEnd() + '\n';
|