@eyoben/auth-postgres 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +44 -3
- package/dist/index.d.ts +18 -0
- package/dist/index.js +288 -0
- package/package.json +35 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ebenezer Seyoum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @eyoben/auth-postgres
|
|
2
|
+
|
|
3
|
+
PostgreSQL and Neon storage adapter for `@eyoben/auth`. The application owns the `postgres` client and connection string.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import postgres from "postgres";
|
|
7
|
+
import { createPostgresAuth, postgresMigration } from "@eyoben/auth-postgres";
|
|
8
|
+
|
|
9
|
+
const sql = postgres(process.env.DATABASE_URL!);
|
|
10
|
+
await sql.unsafe(postgresMigration);
|
|
11
|
+
|
|
12
|
+
export const auth = createPostgresAuth({
|
|
13
|
+
sql,
|
|
14
|
+
baseURL: process.env.APP_URL!,
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`createPostgresAuth` wires the normal authentication adapter together with the
|
|
19
|
+
tenant, identity, membership, verification, and invitation repositories. To
|
|
20
|
+
customize only the domain storage, pass `domain.repositories` in the options.
|
|
21
|
+
|
|
22
|
+
Run `await auth.domain?.cleanupExpiredInvitations()` from your application's
|
|
23
|
+
cron worker. It deletes only expired invitations that have not been accepted.
|
|
24
|
+
|
|
25
|
+
For installations created before nullable email support, run
|
|
26
|
+
`postgresNullableEmailMigration` once before enabling phone-only signup.
|
|
27
|
+
|
|
28
|
+
Do not put connection strings or migrations inside a reusable application package.
|
|
29
|
+
|
|
30
|
+
## Live test
|
|
31
|
+
|
|
32
|
+
Use a disposable PostgreSQL or Neon database. In PowerShell, set the connection
|
|
33
|
+
string for the current terminal session without committing it:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
$env:DATABASE_URL = "postgres://user:password@host/database?sslmode=require"
|
|
37
|
+
npm run test:live --workspace=@eyoben/auth-postgres
|
|
38
|
+
Remove-Item Env:DATABASE_URL
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The live test creates the Eyoben tables, inserts a uniquely named test user,
|
|
42
|
+
creates and reads a session, deletes the session, and removes the test user.
|
|
43
|
+
Without `DATABASE_URL`, it is skipped. Use a disposable database because the
|
|
44
|
+
migration creates shared tables in the selected database.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AuthDomainRepositories, Adapter, CreateAuthConfig, Auth } from '@eyoben/auth';
|
|
2
|
+
|
|
3
|
+
declare function createPostgresDomainRepositories(sql: PostgresClient): AuthDomainRepositories;
|
|
4
|
+
|
|
5
|
+
type PostgresClient = {
|
|
6
|
+
unsafe<T>(query: string, values?: unknown[]): Promise<T[]>;
|
|
7
|
+
begin?<T>(callback: (sql: PostgresClient) => Promise<T>): Promise<T>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
declare const postgresMigration = "\nCREATE TABLE IF NOT EXISTS \"user\" (\n id text PRIMARY KEY, email text UNIQUE, email_verified boolean NOT NULL DEFAULT false,\n name text, password_hash text, two_factor_enabled boolean NOT NULL DEFAULT false,\n created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL\n);\nCREATE TABLE IF NOT EXISTS session (\n id text PRIMARY KEY, user_id text NOT NULL REFERENCES \"user\"(id) ON DELETE CASCADE,\n token_hash text NOT NULL UNIQUE, expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL,\n ip_address text, user_agent text\n);\nCREATE TABLE IF NOT EXISTS account (\n id text PRIMARY KEY, user_id text NOT NULL REFERENCES \"user\"(id) ON DELETE CASCADE,\n provider_id text NOT NULL, account_id text NOT NULL, created_at timestamptz NOT NULL,\n UNIQUE(provider_id, account_id)\n);\nCREATE TABLE IF NOT EXISTS verification (\n id text PRIMARY KEY, identifier text NOT NULL, token_hash text NOT NULL UNIQUE,\n type text NOT NULL CHECK (type IN ('email-verification', 'password-reset')),\n expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL\n);\nCREATE TABLE IF NOT EXISTS passkey (\n id text PRIMARY KEY, user_id text NOT NULL REFERENCES \"user\"(id) ON DELETE CASCADE,\n credential_id text NOT NULL UNIQUE, public_key text NOT NULL, counter integer NOT NULL,\n transports text, device_type text NOT NULL, backed_up boolean NOT NULL, name text,\n created_at timestamptz NOT NULL, last_used_at timestamptz\n);\nCREATE TABLE IF NOT EXISTS two_factor_secret (\n id text PRIMARY KEY, user_id text NOT NULL UNIQUE REFERENCES \"user\"(id) ON DELETE CASCADE,\n encrypted_secret text NOT NULL, created_at timestamptz NOT NULL\n);\nCREATE TABLE IF NOT EXISTS recovery_code (\n id text PRIMARY KEY, user_id text NOT NULL REFERENCES \"user\"(id) ON DELETE CASCADE,\n code_hash text NOT NULL, used_at timestamptz, created_at timestamptz NOT NULL\n);\nCREATE TABLE IF NOT EXISTS organization (\n id text PRIMARY KEY, name text NOT NULL, slug text NOT NULL UNIQUE, created_at timestamptz NOT NULL\n);\nCREATE TABLE IF NOT EXISTS organization_member (\n id text PRIMARY KEY, organization_id text NOT NULL REFERENCES organization(id) ON DELETE CASCADE,\n user_id text NOT NULL REFERENCES \"user\"(id) ON DELETE CASCADE,\n role text NOT NULL CHECK (role IN ('owner', 'admin', 'member')), created_at timestamptz NOT NULL,\n UNIQUE(organization_id, user_id)\n);\nCREATE INDEX IF NOT EXISTS session_token_hash_idx ON session(token_hash);\nCREATE INDEX IF NOT EXISTS passkey_user_id_idx ON passkey(user_id);\nCREATE TABLE IF NOT EXISTS auth_identity (id text PRIMARY KEY,user_id text NOT NULL REFERENCES \"user\"(id) ON DELETE CASCADE,kind text NOT NULL,value text NOT NULL,provider text,verified_at timestamptz,created_at timestamptz NOT NULL,UNIQUE(kind,value,provider));\nCREATE TABLE IF NOT EXISTS auth_tenant (id text PRIMARY KEY,name text NOT NULL,slug text NOT NULL UNIQUE,active boolean NOT NULL DEFAULT true,created_at timestamptz NOT NULL);\nCREATE TABLE IF NOT EXISTS auth_membership (id text PRIMARY KEY,tenant_id text NOT NULL REFERENCES auth_tenant(id) ON DELETE CASCADE,user_id text NOT NULL REFERENCES \"user\"(id) ON DELETE CASCADE,role text NOT NULL,status text NOT NULL DEFAULT 'active',suspended_at timestamptz,suspension_reason text,created_at timestamptz NOT NULL,UNIQUE(tenant_id,user_id));\nCREATE TABLE IF NOT EXISTS auth_challenge (id text PRIMARY KEY,purpose text NOT NULL,subject text NOT NULL,tenant_id text REFERENCES auth_tenant(id) ON DELETE CASCADE,token_hash text NOT NULL UNIQUE,expires_at timestamptz NOT NULL,attempts integer NOT NULL DEFAULT 0,max_attempts integer NOT NULL,consumed_at timestamptz,created_at timestamptz NOT NULL);\nCREATE TABLE IF NOT EXISTS auth_invitation (id text PRIMARY KEY,tenant_id text NOT NULL REFERENCES auth_tenant(id) ON DELETE CASCADE,identifier text NOT NULL,role text NOT NULL,token_hash text NOT NULL UNIQUE,expires_at timestamptz NOT NULL,accepted_at timestamptz,created_at timestamptz NOT NULL);\n";
|
|
11
|
+
/** Apply after upgrading an installation created before phone-only accounts. */
|
|
12
|
+
declare const postgresNullableEmailMigration = "ALTER TABLE \"user\" ALTER COLUMN email DROP NOT NULL;";
|
|
13
|
+
declare function createPostgresAdapter(sql: PostgresClient): Adapter;
|
|
14
|
+
declare function createPostgresAuth(config: Omit<CreateAuthConfig, "adapter" | "db"> & {
|
|
15
|
+
sql: PostgresClient;
|
|
16
|
+
}): Auth;
|
|
17
|
+
|
|
18
|
+
export { type PostgresClient, createPostgresAdapter, createPostgresAuth, createPostgresDomainRepositories, postgresMigration, postgresNullableEmailMigration };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createAuth } from "@eyoben/auth";
|
|
3
|
+
|
|
4
|
+
// src/domain-adapter.ts
|
|
5
|
+
var id = () => crypto.randomUUID();
|
|
6
|
+
var now = () => /* @__PURE__ */ new Date();
|
|
7
|
+
var first = (rows) => rows[0] ?? null;
|
|
8
|
+
function createPostgresDomainRepositories(sql) {
|
|
9
|
+
const identities = {
|
|
10
|
+
async createIdentity(input) {
|
|
11
|
+
return first(await sql.unsafe(`INSERT INTO auth_identity(id,user_id,kind,value,provider,verified_at,created_at) VALUES($1,$2,$3,$4,$5,$6,$7) RETURNING id,user_id AS "userId",kind,value,provider,verified_at AS "verifiedAt",created_at AS "createdAt"`, [id(), input.userId, input.kind, input.value, input.provider ?? null, input.verifiedAt, now()]));
|
|
12
|
+
},
|
|
13
|
+
async findIdentity(kind, value, provider) {
|
|
14
|
+
return first(await sql.unsafe(`SELECT id,user_id AS "userId",kind,value,provider,verified_at AS "verifiedAt",created_at AS "createdAt" FROM auth_identity WHERE kind=$1 AND value=$2 AND provider IS NOT DISTINCT FROM $3 LIMIT 1`, [kind, value, provider ?? null]));
|
|
15
|
+
},
|
|
16
|
+
async listIdentities(userId) {
|
|
17
|
+
return sql.unsafe(`SELECT id,user_id AS "userId",kind,value,provider,verified_at AS "verifiedAt",created_at AS "createdAt" FROM auth_identity WHERE user_id=$1`, [userId]);
|
|
18
|
+
},
|
|
19
|
+
async markIdentityVerified(identityId, verifiedAt) {
|
|
20
|
+
await sql.unsafe(`UPDATE auth_identity SET verified_at=$2 WHERE id=$1`, [identityId, verifiedAt]);
|
|
21
|
+
},
|
|
22
|
+
async deleteIdentity(identityId, userId) {
|
|
23
|
+
await sql.unsafe(`DELETE FROM auth_identity WHERE id=$1 AND user_id=$2`, [identityId, userId]);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const tenants = {
|
|
27
|
+
async createTenant(input) {
|
|
28
|
+
return first(await sql.unsafe(`INSERT INTO auth_tenant(id,name,slug,active,created_at) VALUES($1,$2,$3,true,$4) RETURNING id,name,slug,active,created_at AS "createdAt"`, [id(), input.name, input.slug, now()]));
|
|
29
|
+
},
|
|
30
|
+
async getTenant(tenantId) {
|
|
31
|
+
return first(await sql.unsafe(`SELECT id,name,slug,active,created_at AS "createdAt" FROM auth_tenant WHERE id=$1`, [tenantId]));
|
|
32
|
+
},
|
|
33
|
+
async getTenantBySlug(slug) {
|
|
34
|
+
return first(await sql.unsafe(`SELECT id,name,slug,active,created_at AS "createdAt" FROM auth_tenant WHERE slug=$1`, [slug]));
|
|
35
|
+
},
|
|
36
|
+
async getMembership(tenantId, userId) {
|
|
37
|
+
return first(await sql.unsafe(`SELECT id,tenant_id AS "tenantId",user_id AS "userId",role,created_at AS "createdAt" FROM auth_membership WHERE tenant_id=$1 AND user_id=$2`, [tenantId, userId]));
|
|
38
|
+
},
|
|
39
|
+
async listMemberships(userId) {
|
|
40
|
+
return sql.unsafe(`SELECT id,tenant_id AS "tenantId",user_id AS "userId",role,created_at AS "createdAt" FROM auth_membership WHERE user_id=$1`, [userId]);
|
|
41
|
+
},
|
|
42
|
+
async listTenantMemberships(tenantId) {
|
|
43
|
+
return sql.unsafe(`SELECT id,tenant_id AS "tenantId",user_id AS "userId",role,created_at AS "createdAt" FROM auth_membership WHERE tenant_id=$1`, [tenantId]);
|
|
44
|
+
},
|
|
45
|
+
async addMembership(input) {
|
|
46
|
+
return first(await sql.unsafe(`INSERT INTO auth_membership(id,tenant_id,user_id,role,created_at) VALUES($1,$2,$3,$4,$5) RETURNING id,tenant_id AS "tenantId",user_id AS "userId",role,created_at AS "createdAt"`, [id(), input.tenantId, input.userId, input.role, now()]));
|
|
47
|
+
},
|
|
48
|
+
async changeMembershipRole(tenantId, userId, role) {
|
|
49
|
+
await sql.unsafe(`UPDATE auth_membership SET role=$3 WHERE tenant_id=$1 AND user_id=$2`, [tenantId, userId, role]);
|
|
50
|
+
},
|
|
51
|
+
async removeMembership(tenantId, userId) {
|
|
52
|
+
await sql.unsafe(`DELETE FROM auth_membership WHERE tenant_id=$1 AND user_id=$2`, [tenantId, userId]);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
tenants.setMembershipStatus = async (t, u, status, d, reason) => {
|
|
56
|
+
await sql.unsafe(`UPDATE auth_membership SET status=$3,suspended_at=$4,suspension_reason=$5 WHERE tenant_id=$1 AND user_id=$2`, [t, u, status, d, reason]);
|
|
57
|
+
};
|
|
58
|
+
const verifications = {
|
|
59
|
+
async createChallenge(input) {
|
|
60
|
+
return first(await sql.unsafe(`INSERT INTO auth_challenge(id,purpose,subject,tenant_id,token_hash,expires_at,attempts,max_attempts,created_at) VALUES($1,$2,$3,$4,$5,$6,0,$7,$8) RETURNING id,purpose,subject,tenant_id AS "tenantId",token_hash AS "tokenHash",expires_at AS "expiresAt",attempts,max_attempts AS "maxAttempts",consumed_at AS "consumedAt",created_at AS "createdAt"`, [id(), input.purpose, input.subject, input.tenantId ?? null, input.tokenHash, input.expiresAt, input.maxAttempts, now()]));
|
|
61
|
+
},
|
|
62
|
+
async getChallenge(tokenHash, purpose) {
|
|
63
|
+
return first(await sql.unsafe(`SELECT id,purpose,subject,tenant_id AS "tenantId",token_hash AS "tokenHash",expires_at AS "expiresAt",attempts,max_attempts AS "maxAttempts",consumed_at AS "consumedAt",created_at AS "createdAt" FROM auth_challenge WHERE token_hash=$1 AND purpose=$2`, [tokenHash, purpose]));
|
|
64
|
+
},
|
|
65
|
+
async incrementAttempts(challengeId) {
|
|
66
|
+
const rows = await sql.unsafe(`UPDATE auth_challenge SET attempts=attempts+1 WHERE id=$1 RETURNING attempts`, [challengeId]);
|
|
67
|
+
return rows[0]?.attempts ?? 0;
|
|
68
|
+
},
|
|
69
|
+
async consumeChallenge(challengeId, consumedAt) {
|
|
70
|
+
await sql.unsafe(`UPDATE auth_challenge SET consumed_at=$2 WHERE id=$1`, [challengeId, consumedAt]);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const invitations = {
|
|
74
|
+
async createInvitation(input) {
|
|
75
|
+
return first(await sql.unsafe(`INSERT INTO auth_invitation(id,tenant_id,identifier,role,token_hash,expires_at,created_at) VALUES($1,$2,$3,$4,$5,$6,$7) RETURNING id,tenant_id AS "tenantId",identifier,role,token_hash AS "tokenHash",expires_at AS "expiresAt",accepted_at AS "acceptedAt",created_at AS "createdAt"`, [id(), input.tenantId, input.identifier, input.role, input.tokenHash, input.expiresAt, now()]));
|
|
76
|
+
},
|
|
77
|
+
async getInvitation(tokenHash) {
|
|
78
|
+
return first(await sql.unsafe(`SELECT id,tenant_id AS "tenantId",identifier,role,token_hash AS "tokenHash",expires_at AS "expiresAt",accepted_at AS "acceptedAt",created_at AS "createdAt" FROM auth_invitation WHERE token_hash=$1`, [tokenHash]));
|
|
79
|
+
},
|
|
80
|
+
async acceptInvitation(invitationId, acceptedAt) {
|
|
81
|
+
await sql.unsafe(`UPDATE auth_invitation SET accepted_at=$2 WHERE id=$1`, [invitationId, acceptedAt]);
|
|
82
|
+
},
|
|
83
|
+
async deleteInvitation(invitationId) {
|
|
84
|
+
await sql.unsafe(`DELETE FROM auth_invitation WHERE id=$1`, [invitationId]);
|
|
85
|
+
},
|
|
86
|
+
async deleteExpiredInvitations(expiry) {
|
|
87
|
+
const rows = await sql.unsafe(`DELETE FROM auth_invitation WHERE expires_at <= $1 AND accepted_at IS NULL RETURNING id`, [expiry]);
|
|
88
|
+
return rows.length;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const atomicAcceptInvitation = sql.begin ? async (input) => sql.begin(async (tx) => {
|
|
92
|
+
const claimed = await tx.unsafe(`UPDATE auth_invitation SET accepted_at=$2 WHERE id=$1 AND accepted_at IS NULL RETURNING id`, [input.invitationId, input.acceptedAt]);
|
|
93
|
+
if (!claimed.length) throw new Error("Invitation already accepted");
|
|
94
|
+
const rows = await tx.unsafe(`INSERT INTO auth_membership(id,tenant_id,user_id,role,created_at) VALUES($1,$2,$3,$4,$5) RETURNING id,tenant_id AS "tenantId",user_id AS "userId",role,created_at AS "createdAt"`, [id(), input.tenantId, input.userId, input.role, now()]);
|
|
95
|
+
return first(rows);
|
|
96
|
+
}) : void 0;
|
|
97
|
+
return { identities, tenants, verifications, invitations, atomicAcceptInvitation };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/index.ts
|
|
101
|
+
var postgresMigration = `
|
|
102
|
+
CREATE TABLE IF NOT EXISTS "user" (
|
|
103
|
+
id text PRIMARY KEY, email text UNIQUE, email_verified boolean NOT NULL DEFAULT false,
|
|
104
|
+
name text, password_hash text, two_factor_enabled boolean NOT NULL DEFAULT false,
|
|
105
|
+
created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
|
|
106
|
+
);
|
|
107
|
+
CREATE TABLE IF NOT EXISTS session (
|
|
108
|
+
id text PRIMARY KEY, user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
|
109
|
+
token_hash text NOT NULL UNIQUE, expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL,
|
|
110
|
+
ip_address text, user_agent text
|
|
111
|
+
);
|
|
112
|
+
CREATE TABLE IF NOT EXISTS account (
|
|
113
|
+
id text PRIMARY KEY, user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
|
114
|
+
provider_id text NOT NULL, account_id text NOT NULL, created_at timestamptz NOT NULL,
|
|
115
|
+
UNIQUE(provider_id, account_id)
|
|
116
|
+
);
|
|
117
|
+
CREATE TABLE IF NOT EXISTS verification (
|
|
118
|
+
id text PRIMARY KEY, identifier text NOT NULL, token_hash text NOT NULL UNIQUE,
|
|
119
|
+
type text NOT NULL CHECK (type IN ('email-verification', 'password-reset')),
|
|
120
|
+
expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL
|
|
121
|
+
);
|
|
122
|
+
CREATE TABLE IF NOT EXISTS passkey (
|
|
123
|
+
id text PRIMARY KEY, user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
|
124
|
+
credential_id text NOT NULL UNIQUE, public_key text NOT NULL, counter integer NOT NULL,
|
|
125
|
+
transports text, device_type text NOT NULL, backed_up boolean NOT NULL, name text,
|
|
126
|
+
created_at timestamptz NOT NULL, last_used_at timestamptz
|
|
127
|
+
);
|
|
128
|
+
CREATE TABLE IF NOT EXISTS two_factor_secret (
|
|
129
|
+
id text PRIMARY KEY, user_id text NOT NULL UNIQUE REFERENCES "user"(id) ON DELETE CASCADE,
|
|
130
|
+
encrypted_secret text NOT NULL, created_at timestamptz NOT NULL
|
|
131
|
+
);
|
|
132
|
+
CREATE TABLE IF NOT EXISTS recovery_code (
|
|
133
|
+
id text PRIMARY KEY, user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
|
134
|
+
code_hash text NOT NULL, used_at timestamptz, created_at timestamptz NOT NULL
|
|
135
|
+
);
|
|
136
|
+
CREATE TABLE IF NOT EXISTS organization (
|
|
137
|
+
id text PRIMARY KEY, name text NOT NULL, slug text NOT NULL UNIQUE, created_at timestamptz NOT NULL
|
|
138
|
+
);
|
|
139
|
+
CREATE TABLE IF NOT EXISTS organization_member (
|
|
140
|
+
id text PRIMARY KEY, organization_id text NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
|
|
141
|
+
user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
|
142
|
+
role text NOT NULL CHECK (role IN ('owner', 'admin', 'member')), created_at timestamptz NOT NULL,
|
|
143
|
+
UNIQUE(organization_id, user_id)
|
|
144
|
+
);
|
|
145
|
+
CREATE INDEX IF NOT EXISTS session_token_hash_idx ON session(token_hash);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS passkey_user_id_idx ON passkey(user_id);
|
|
147
|
+
CREATE TABLE IF NOT EXISTS auth_identity (id text PRIMARY KEY,user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,kind text NOT NULL,value text NOT NULL,provider text,verified_at timestamptz,created_at timestamptz NOT NULL,UNIQUE(kind,value,provider));
|
|
148
|
+
CREATE TABLE IF NOT EXISTS auth_tenant (id text PRIMARY KEY,name text NOT NULL,slug text NOT NULL UNIQUE,active boolean NOT NULL DEFAULT true,created_at timestamptz NOT NULL);
|
|
149
|
+
CREATE TABLE IF NOT EXISTS auth_membership (id text PRIMARY KEY,tenant_id text NOT NULL REFERENCES auth_tenant(id) ON DELETE CASCADE,user_id text NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,role text NOT NULL,status text NOT NULL DEFAULT 'active',suspended_at timestamptz,suspension_reason text,created_at timestamptz NOT NULL,UNIQUE(tenant_id,user_id));
|
|
150
|
+
CREATE TABLE IF NOT EXISTS auth_challenge (id text PRIMARY KEY,purpose text NOT NULL,subject text NOT NULL,tenant_id text REFERENCES auth_tenant(id) ON DELETE CASCADE,token_hash text NOT NULL UNIQUE,expires_at timestamptz NOT NULL,attempts integer NOT NULL DEFAULT 0,max_attempts integer NOT NULL,consumed_at timestamptz,created_at timestamptz NOT NULL);
|
|
151
|
+
CREATE TABLE IF NOT EXISTS auth_invitation (id text PRIMARY KEY,tenant_id text NOT NULL REFERENCES auth_tenant(id) ON DELETE CASCADE,identifier text NOT NULL,role text NOT NULL,token_hash text NOT NULL UNIQUE,expires_at timestamptz NOT NULL,accepted_at timestamptz,created_at timestamptz NOT NULL);
|
|
152
|
+
`;
|
|
153
|
+
var postgresNullableEmailMigration = `ALTER TABLE "user" ALTER COLUMN email DROP NOT NULL;`;
|
|
154
|
+
var id2 = () => crypto.randomUUID();
|
|
155
|
+
var now2 = () => /* @__PURE__ */ new Date();
|
|
156
|
+
var first2 = (rows) => rows[0] ?? null;
|
|
157
|
+
function createPostgresAdapter(sql) {
|
|
158
|
+
return {
|
|
159
|
+
async createUser(input) {
|
|
160
|
+
return first2(await sql.unsafe(`INSERT INTO "user" (id,email,password_hash,name,email_verified,created_at,updated_at) VALUES ($1,$2,$3,$4,$5,$6,$6) RETURNING id,email,password_hash,name,email_verified AS "emailVerified",two_factor_enabled AS "twoFactorEnabled",created_at AS "createdAt",updated_at AS "updatedAt"`, [id2(), input.email, input.passwordHash, input.name, input.emailVerified ?? false, now2()]));
|
|
161
|
+
},
|
|
162
|
+
async getUserByEmail(email) {
|
|
163
|
+
return first2(await sql.unsafe(`SELECT id,email,password_hash AS "passwordHash",name,email_verified AS "emailVerified",two_factor_enabled AS "twoFactorEnabled",created_at AS "createdAt",updated_at AS "updatedAt" FROM "user" WHERE email=$1 LIMIT 1`, [email]));
|
|
164
|
+
},
|
|
165
|
+
async getUserById(userId) {
|
|
166
|
+
return first2(await sql.unsafe(`SELECT id,email,password_hash AS "passwordHash",name,email_verified AS "emailVerified",two_factor_enabled AS "twoFactorEnabled",created_at AS "createdAt",updated_at AS "updatedAt" FROM "user" WHERE id=$1 LIMIT 1`, [userId]));
|
|
167
|
+
},
|
|
168
|
+
async markEmailVerified(userId) {
|
|
169
|
+
await sql.unsafe(`UPDATE "user" SET email_verified=true,updated_at=$2 WHERE id=$1`, [userId, now2()]);
|
|
170
|
+
},
|
|
171
|
+
async updateUserPassword(userId, passwordHash) {
|
|
172
|
+
await sql.unsafe(`UPDATE "user" SET password_hash=$2,updated_at=$3 WHERE id=$1`, [userId, passwordHash, now2()]);
|
|
173
|
+
},
|
|
174
|
+
async linkAccount(input) {
|
|
175
|
+
await sql.unsafe(`INSERT INTO account(id,user_id,provider_id,account_id,created_at) VALUES($1,$2,$3,$4,$5)`, [id2(), input.userId, input.providerId, input.accountId, now2()]);
|
|
176
|
+
},
|
|
177
|
+
async getAccountByProvider(providerId, accountId) {
|
|
178
|
+
return first2(await sql.unsafe(`SELECT id,user_id AS "userId",provider_id AS "providerId",account_id AS "accountId",created_at AS "createdAt" FROM account WHERE provider_id=$1 AND account_id=$2 LIMIT 1`, [providerId, accountId]));
|
|
179
|
+
},
|
|
180
|
+
async createSession(input) {
|
|
181
|
+
return first2(await sql.unsafe(`INSERT INTO session(id,user_id,token_hash,expires_at,created_at,ip_address,user_agent) VALUES($1,$2,$3,$4,$5,$6,$7) RETURNING id,user_id AS "userId",token_hash AS "tokenHash",expires_at AS "expiresAt",created_at AS "createdAt",ip_address AS "ipAddress",user_agent AS "userAgent"`, [input.id, input.userId, input.tokenHash, input.expiresAt, now2(), input.ipAddress, input.userAgent]));
|
|
182
|
+
},
|
|
183
|
+
async getSessionWithUser(tokenHash) {
|
|
184
|
+
return first2(await sql.unsafe(`SELECT json_build_object('id',s.id,'userId',s.user_id,'tokenHash',s.token_hash,'expiresAt',s.expires_at,'createdAt',s.created_at,'ipAddress',s.ip_address,'userAgent',s.user_agent) AS session,json_build_object('id',u.id,'email',u.email,'passwordHash',u.password_hash,'name',u.name,'emailVerified',u.email_verified,'twoFactorEnabled',u.two_factor_enabled,'createdAt',u.created_at,'updatedAt',u.updated_at) AS user FROM session s JOIN "user" u ON u.id=s.user_id WHERE s.token_hash=$1 LIMIT 1`, [tokenHash]));
|
|
185
|
+
},
|
|
186
|
+
async listUserSessions(userId, now3 = /* @__PURE__ */ new Date()) {
|
|
187
|
+
return sql.unsafe(`SELECT id,user_id AS "userId",token_hash AS "tokenHash",expires_at AS "expiresAt",created_at AS "createdAt",ip_address AS "ipAddress",user_agent AS "userAgent" FROM session WHERE user_id=$1 AND expires_at>$2 ORDER BY created_at DESC`, [userId, now3]);
|
|
188
|
+
},
|
|
189
|
+
async deleteSession(sessionId) {
|
|
190
|
+
await sql.unsafe(`DELETE FROM session WHERE id=$1`, [sessionId]);
|
|
191
|
+
},
|
|
192
|
+
async deleteAllUserSessions(userId) {
|
|
193
|
+
await sql.unsafe(`DELETE FROM session WHERE user_id=$1`, [userId]);
|
|
194
|
+
},
|
|
195
|
+
async createVerification(input) {
|
|
196
|
+
return first2(await sql.unsafe(`INSERT INTO verification(id,identifier,token_hash,type,expires_at,created_at) VALUES($1,$2,$3,$4,$5,$6) RETURNING id,identifier,token_hash AS "tokenHash",type,expires_at AS "expiresAt",created_at AS "createdAt"`, [id2(), input.identifier, input.tokenHash, input.type, input.expiresAt, now2()]));
|
|
197
|
+
},
|
|
198
|
+
async getVerification(tokenHash, type) {
|
|
199
|
+
return first2(await sql.unsafe(`SELECT id,identifier,token_hash AS "tokenHash",type,expires_at AS "expiresAt",created_at AS "createdAt" FROM verification WHERE token_hash=$1 AND type=$2 LIMIT 1`, [tokenHash, type]));
|
|
200
|
+
},
|
|
201
|
+
async deleteVerification(verificationId) {
|
|
202
|
+
await sql.unsafe(`DELETE FROM verification WHERE id=$1`, [verificationId]);
|
|
203
|
+
},
|
|
204
|
+
async createPasskey(input) {
|
|
205
|
+
return first2(await sql.unsafe(`INSERT INTO passkey(id,user_id,credential_id,public_key,counter,transports,device_type,backed_up,name,created_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING id,user_id AS "userId",credential_id AS "credentialId",public_key AS "publicKey",counter,transports,device_type AS "deviceType",backed_up AS "backedUp",name,created_at AS "createdAt",last_used_at AS "lastUsedAt"`, [id2(), input.userId, input.credentialId, input.publicKey, input.counter, input.transports, input.deviceType, input.backedUp, input.name, now2()]));
|
|
206
|
+
},
|
|
207
|
+
async getPasskeyByCredentialId(credentialId) {
|
|
208
|
+
return first2(await sql.unsafe(`SELECT id,user_id AS "userId",credential_id AS "credentialId",public_key AS "publicKey",counter,transports,device_type AS "deviceType",backed_up AS "backedUp",name,created_at AS "createdAt",last_used_at AS "lastUsedAt" FROM passkey WHERE credential_id=$1 LIMIT 1`, [credentialId]));
|
|
209
|
+
},
|
|
210
|
+
async getPasskeysByUserId(userId) {
|
|
211
|
+
return sql.unsafe(`SELECT id,user_id AS "userId",credential_id AS "credentialId",public_key AS "publicKey",counter,transports,device_type AS "deviceType",backed_up AS "backedUp",name,created_at AS "createdAt",last_used_at AS "lastUsedAt" FROM passkey WHERE user_id=$1`, [userId]);
|
|
212
|
+
},
|
|
213
|
+
async updatePasskeyCounter(passkeyId, counter) {
|
|
214
|
+
await sql.unsafe(`UPDATE passkey SET counter=$2,last_used_at=$3 WHERE id=$1`, [passkeyId, counter, now2()]);
|
|
215
|
+
},
|
|
216
|
+
async deletePasskey(passkeyId, userId) {
|
|
217
|
+
await sql.unsafe(`DELETE FROM passkey WHERE id=$1 AND user_id=$2`, [passkeyId, userId]);
|
|
218
|
+
},
|
|
219
|
+
async setTwoFactorSecret(userId, encryptedSecret) {
|
|
220
|
+
await sql.unsafe(`INSERT INTO two_factor_secret(id,user_id,encrypted_secret,created_at) VALUES($1,$2,$3,$4) ON CONFLICT(user_id) DO UPDATE SET encrypted_secret=EXCLUDED.encrypted_secret,created_at=EXCLUDED.created_at`, [id2(), userId, encryptedSecret, now2()]);
|
|
221
|
+
},
|
|
222
|
+
async getTwoFactorSecret(userId) {
|
|
223
|
+
return first2(await sql.unsafe(`SELECT id,user_id AS "userId",encrypted_secret AS "encryptedSecret",created_at AS "createdAt" FROM two_factor_secret WHERE user_id=$1 LIMIT 1`, [userId]));
|
|
224
|
+
},
|
|
225
|
+
async deleteTwoFactorSecret(userId) {
|
|
226
|
+
await sql.unsafe(`DELETE FROM two_factor_secret WHERE user_id=$1`, [userId]);
|
|
227
|
+
},
|
|
228
|
+
async setTwoFactorEnabled(userId, enabled) {
|
|
229
|
+
await sql.unsafe(`UPDATE "user" SET two_factor_enabled=$2,updated_at=$3 WHERE id=$1`, [userId, enabled, now2()]);
|
|
230
|
+
},
|
|
231
|
+
async createRecoveryCodes(userId, codeHashes) {
|
|
232
|
+
if (codeHashes.length) await Promise.all(codeHashes.map((codeHash) => sql.unsafe(`INSERT INTO recovery_code(id,user_id,code_hash,created_at) VALUES($1,$2,$3,$4)`, [id2(), userId, codeHash, now2()])));
|
|
233
|
+
},
|
|
234
|
+
async getUnusedRecoveryCode(userId, codeHash) {
|
|
235
|
+
return first2(await sql.unsafe(`SELECT id,user_id AS "userId",code_hash AS "codeHash",used_at AS "usedAt",created_at AS "createdAt" FROM recovery_code WHERE user_id=$1 AND code_hash=$2 AND used_at IS NULL LIMIT 1`, [userId, codeHash]));
|
|
236
|
+
},
|
|
237
|
+
async markRecoveryCodeUsed(codeId) {
|
|
238
|
+
await sql.unsafe(`UPDATE recovery_code SET used_at=$2 WHERE id=$1`, [codeId, now2()]);
|
|
239
|
+
},
|
|
240
|
+
async deleteRecoveryCodes(userId) {
|
|
241
|
+
await sql.unsafe(`DELETE FROM recovery_code WHERE user_id=$1`, [userId]);
|
|
242
|
+
},
|
|
243
|
+
async createOrganization(input) {
|
|
244
|
+
const org = first2(await sql.unsafe(`INSERT INTO organization(id,name,slug,created_at) VALUES($1,$2,$3,$4) RETURNING id,name,slug,created_at AS "createdAt"`, [id2(), input.name, input.slug, now2()]));
|
|
245
|
+
await sql.unsafe(`INSERT INTO organization_member(id,organization_id,user_id,role,created_at) VALUES($1,$2,$3,'owner',$4)`, [id2(), org.id, input.ownerId, now2()]);
|
|
246
|
+
return org;
|
|
247
|
+
},
|
|
248
|
+
async getOrganizationById(orgId) {
|
|
249
|
+
return first2(await sql.unsafe(`SELECT id,name,slug,created_at AS "createdAt" FROM organization WHERE id=$1 LIMIT 1`, [orgId]));
|
|
250
|
+
},
|
|
251
|
+
async getOrganizationBySlug(slug) {
|
|
252
|
+
return first2(await sql.unsafe(`SELECT id,name,slug,created_at AS "createdAt" FROM organization WHERE slug=$1 LIMIT 1`, [slug]));
|
|
253
|
+
},
|
|
254
|
+
async getOrganizationsForUser(userId) {
|
|
255
|
+
return sql.unsafe(`SELECT json_build_object('id',o.id,'name',o.name,'slug',o.slug,'createdAt',o.created_at) AS organization,m.role FROM organization_member m JOIN organization o ON o.id=m.organization_id WHERE m.user_id=$1`, [userId]);
|
|
256
|
+
},
|
|
257
|
+
async addOrganizationMember(input) {
|
|
258
|
+
return first2(await sql.unsafe(`INSERT INTO organization_member(id,organization_id,user_id,role,created_at) VALUES($1,$2,$3,$4,$5) RETURNING id,organization_id AS "organizationId",user_id AS "userId",role,created_at AS "createdAt"`, [id2(), input.organizationId, input.userId, input.role, now2()]));
|
|
259
|
+
},
|
|
260
|
+
async getOrganizationMember(organizationId, userId) {
|
|
261
|
+
return first2(await sql.unsafe(`SELECT id,organization_id AS "organizationId",user_id AS "userId",role,created_at AS "createdAt" FROM organization_member WHERE organization_id=$1 AND user_id=$2 LIMIT 1`, [organizationId, userId]));
|
|
262
|
+
},
|
|
263
|
+
async getOrganizationMembers(organizationId) {
|
|
264
|
+
return sql.unsafe(`SELECT json_build_object('id',m.id,'organizationId',m.organization_id,'userId',m.user_id,'role',m.role,'createdAt',m.created_at) AS member,json_build_object('id',u.id,'email',u.email,'passwordHash',u.password_hash,'name',u.name,'emailVerified',u.email_verified,'twoFactorEnabled',u.two_factor_enabled,'createdAt',u.created_at,'updatedAt',u.updated_at) AS user FROM organization_member m JOIN "user" u ON u.id=m.user_id WHERE m.organization_id=$1`, [organizationId]);
|
|
265
|
+
},
|
|
266
|
+
async updateOrganizationMemberRole(organizationId, userId, role) {
|
|
267
|
+
await sql.unsafe(`UPDATE organization_member SET role=$3 WHERE organization_id=$1 AND user_id=$2`, [organizationId, userId, role]);
|
|
268
|
+
},
|
|
269
|
+
async removeOrganizationMember(organizationId, userId) {
|
|
270
|
+
await sql.unsafe(`DELETE FROM organization_member WHERE organization_id=$1 AND user_id=$2`, [organizationId, userId]);
|
|
271
|
+
},
|
|
272
|
+
async countOrganizationOwners(organizationId) {
|
|
273
|
+
const rows = await sql.unsafe(`SELECT count(*)::text AS count FROM organization_member WHERE organization_id=$1 AND role='owner'`, [organizationId]);
|
|
274
|
+
return Number(rows[0]?.count ?? 0);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function createPostgresAuth(config) {
|
|
279
|
+
const { sql, domain, ...authConfig } = config;
|
|
280
|
+
return createAuth({ ...authConfig, adapter: createPostgresAdapter(sql), domain: { ...domain, repositories: domain?.repositories ?? createPostgresDomainRepositories(sql) } });
|
|
281
|
+
}
|
|
282
|
+
export {
|
|
283
|
+
createPostgresAdapter,
|
|
284
|
+
createPostgresAuth,
|
|
285
|
+
createPostgresDomainRepositories,
|
|
286
|
+
postgresMigration,
|
|
287
|
+
postgresNullableEmailMigration
|
|
288
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@eyoben/auth-postgres",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@eyoben/auth-postgres",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PostgreSQL and Neon storage adapter for @eyoben/auth.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"pretest": "npm run build --workspace=@eyoben/auth",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:live": "vitest run src/live.test.ts",
|
|
26
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@eyoben/auth": ">=0.3.0",
|
|
33
|
+
"postgres": ">=3.4.0"
|
|
34
|
+
}
|
|
35
|
+
}
|