@stacksjs/auth 0.70.87 → 0.70.90
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/authentication.js +413 -0
- package/dist/authenticator.js +14 -0
- package/dist/authorizable.js +28 -0
- package/dist/client.js +26 -0
- package/dist/email-verification.js +111 -0
- package/dist/gate.js +203 -0
- package/dist/index.js +26 -165
- package/dist/internal-constants.js +1 -0
- package/dist/middleware.js +28 -0
- package/dist/passkey.js +76 -0
- package/dist/password/reset.js +156 -0
- package/dist/policy.js +91 -0
- package/dist/rate-limiter.js +91 -0
- package/dist/rbac-seed.js +30 -0
- package/dist/rbac-store-bqb.js +180 -0
- package/dist/rbac.js +324 -0
- package/dist/register.js +43 -0
- package/dist/session-auth.d.ts +18 -0
- package/dist/session-auth.js +151 -0
- package/dist/team.js +88 -0
- package/dist/token.js +21 -0
- package/dist/tokens.js +489 -0
- package/dist/two-factor.js +113 -0
- package/dist/user.js +41 -0
- package/package.json +3 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { config } from "@stacksjs/config";
|
|
3
|
+
import { db } from "@stacksjs/database";
|
|
4
|
+
import { mail, template } from "@stacksjs/email";
|
|
5
|
+
import { log } from "@stacksjs/logging";
|
|
6
|
+
import { formatDate } from "@stacksjs/orm";
|
|
7
|
+
import { makeHash, verifyHash } from "@stacksjs/security";
|
|
8
|
+
import { sessionDestroyAll } from "../session-auth";
|
|
9
|
+
import { revokeAllTokens } from "../tokens";
|
|
10
|
+
function getTokenExpireMinutes() {
|
|
11
|
+
return config.auth.passwordReset?.expire ?? 60;
|
|
12
|
+
}
|
|
13
|
+
function isWithinExpiry(row) {
|
|
14
|
+
const explicit = row.expires_at;
|
|
15
|
+
if (typeof explicit === "string" || explicit instanceof Date)
|
|
16
|
+
return new Date(explicit).getTime() > Date.now();
|
|
17
|
+
const created = row.created_at;
|
|
18
|
+
if (typeof created === "string" || created instanceof Date) {
|
|
19
|
+
const expireMinutes = getTokenExpireMinutes();
|
|
20
|
+
return new Date(created).getTime() + expireMinutes * 60000 > Date.now();
|
|
21
|
+
}
|
|
22
|
+
return !1;
|
|
23
|
+
}
|
|
24
|
+
async function sendPasswordChangedNotification(userEmail) {
|
|
25
|
+
const appName = config.app.name || "Stacks", supportEmail = config.app.supportEmail || config.email?.from?.address || "", changedAt = new Date().toLocaleString("en-US", {
|
|
26
|
+
dateStyle: "full",
|
|
27
|
+
timeStyle: "short"
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
const { html, text } = await template("password-changed", {
|
|
31
|
+
subject: `Your ${appName} password has been changed`,
|
|
32
|
+
variables: {
|
|
33
|
+
changedAt,
|
|
34
|
+
supportEmail
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if (!html && !text) {
|
|
38
|
+
await mail.send({
|
|
39
|
+
to: userEmail,
|
|
40
|
+
subject: `Your ${appName} password has been changed`,
|
|
41
|
+
text: `Your ${appName} password was changed on ${changedAt}.${supportEmail ? ` If this wasn't you, contact ${supportEmail}.` : ""}`
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await mail.send({
|
|
46
|
+
to: userEmail,
|
|
47
|
+
subject: `Your ${appName} password has been changed`,
|
|
48
|
+
text,
|
|
49
|
+
html
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("[PasswordReset] Failed to send password changed notification:", error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function passwordResets(email) {
|
|
56
|
+
function generateResetToken() {
|
|
57
|
+
return randomBytes(32).toString("hex");
|
|
58
|
+
}
|
|
59
|
+
async function createResetToken() {
|
|
60
|
+
const token = generateResetToken(), hashedToken = await makeHash(token, { algorithm: "bcrypt" }), expireMinutes = getTokenExpireMinutes(), expiresAt = new Date(Date.now() + expireMinutes * 60000).toISOString();
|
|
61
|
+
await db.deleteFrom("password_resets").where("email", "=", email).execute();
|
|
62
|
+
await db.insertInto("password_resets").values({
|
|
63
|
+
email,
|
|
64
|
+
token: hashedToken,
|
|
65
|
+
expires_at: expiresAt
|
|
66
|
+
}).executeTakeFirst();
|
|
67
|
+
return token;
|
|
68
|
+
}
|
|
69
|
+
async function sendEmail() {
|
|
70
|
+
if (!await db.selectFrom("users").where("email", "=", email).selectAll().executeTakeFirst())
|
|
71
|
+
return;
|
|
72
|
+
const token = await createResetToken(), expireMinutes = getTokenExpireMinutes(), appName = config.app.name || "Stacks", base = config.app.url ? `https://${config.app.url}` : `http://localhost:${process.env.PORT || "3000"}`, filled = (config.auth.passwordReset?.url ?? "/password/reset/{token}?email={email}").replace("{token}", token).replace("{email}", encodeURIComponent(email)), resetUrl = /^https?:\/\//.test(filled) ? filled : `${base}${filled.startsWith("/") ? "" : "/"}${filled}`;
|
|
73
|
+
try {
|
|
74
|
+
const { html, text } = await template("password-reset", {
|
|
75
|
+
subject: `Reset Your ${appName} Password`,
|
|
76
|
+
variables: {
|
|
77
|
+
resetUrl,
|
|
78
|
+
expireMinutes
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (!html && !text)
|
|
82
|
+
throw Error("password-reset template missing or rendered empty");
|
|
83
|
+
await mail.send({
|
|
84
|
+
to: email,
|
|
85
|
+
subject: `Reset Your ${appName} Password`,
|
|
86
|
+
text,
|
|
87
|
+
html
|
|
88
|
+
});
|
|
89
|
+
} catch (templateError) {
|
|
90
|
+
const msg = templateError instanceof Error ? templateError.message : String(templateError);
|
|
91
|
+
console.warn(`[PasswordReset] template render failed, sending plain-text fallback: ${msg}`);
|
|
92
|
+
await mail.send({
|
|
93
|
+
to: email,
|
|
94
|
+
subject: `Reset Your ${appName} Password`,
|
|
95
|
+
text: `Reset your password by visiting: ${resetUrl}
|
|
96
|
+
|
|
97
|
+
This link expires in ${expireMinutes} minutes. If you didn't request this, you can safely ignore this email.`
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function verifyToken(token) {
|
|
102
|
+
const result = await db.selectFrom("password_resets").where("email", "=", email).selectAll().executeTakeFirst();
|
|
103
|
+
if (!result)
|
|
104
|
+
return !1;
|
|
105
|
+
if (!isWithinExpiry(result)) {
|
|
106
|
+
await db.deleteFrom("password_resets").where("email", "=", email).execute();
|
|
107
|
+
return !1;
|
|
108
|
+
}
|
|
109
|
+
const hashedToken = result.token;
|
|
110
|
+
return await verifyHash(token, hashedToken);
|
|
111
|
+
}
|
|
112
|
+
async function resetPassword(token, newPassword) {
|
|
113
|
+
const result = await db.transaction(async (rawTrx) => {
|
|
114
|
+
const trx = rawTrx, resetRecord = await trx.selectFrom("password_resets").where("email", "=", email).selectAll().executeTakeFirst();
|
|
115
|
+
if (!resetRecord)
|
|
116
|
+
return { success: !1, message: "Invalid or expired reset token" };
|
|
117
|
+
if (!isWithinExpiry(resetRecord)) {
|
|
118
|
+
await trx.deleteFrom("password_resets").where("email", "=", email).execute();
|
|
119
|
+
return { success: !1, message: "This password reset link has expired. Please request a new one." };
|
|
120
|
+
}
|
|
121
|
+
const hashedToken = resetRecord.token;
|
|
122
|
+
if (!await verifyHash(token, hashedToken))
|
|
123
|
+
return { success: !1, message: "Invalid or expired reset token" };
|
|
124
|
+
const user = await trx.selectFrom("users").where("email", "=", email).selectAll().executeTakeFirst();
|
|
125
|
+
if (!user)
|
|
126
|
+
return { success: !1, message: "Invalid or expired reset token" };
|
|
127
|
+
const hashedPassword = await makeHash(newPassword, { algorithm: "bcrypt" });
|
|
128
|
+
try {
|
|
129
|
+
await trx.updateTable("users").set({ password: hashedPassword, password_changed_at: formatDate(new Date) }).where("email", "=", email).executeTakeFirst();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
+
if (/password_changed_at|no such column|unknown column/i.test(message)) {
|
|
133
|
+
log.warn("[PasswordReset] password_changed_at column missing \u2014 run `buddy migrate`; resetting without the credential-version stamp");
|
|
134
|
+
await trx.updateTable("users").set({ password: hashedPassword }).where("email", "=", email).executeTakeFirst();
|
|
135
|
+
} else
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
await trx.deleteFrom("password_resets").where("email", "=", email).execute();
|
|
139
|
+
return { success: !0, userId: Number(user.id) };
|
|
140
|
+
});
|
|
141
|
+
if (result.success) {
|
|
142
|
+
await revokeAllTokens(result.userId);
|
|
143
|
+
await sessionDestroyAll(result.userId);
|
|
144
|
+
sendPasswordChangedNotification(email).catch((err) => {
|
|
145
|
+
console.error("[PasswordReset] Failed to send notification:", err);
|
|
146
|
+
});
|
|
147
|
+
return { success: !0 };
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
sendEmail,
|
|
153
|
+
verifyToken,
|
|
154
|
+
resetPassword
|
|
155
|
+
};
|
|
156
|
+
}
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AuthorizationResponse } from "./gate";
|
|
2
|
+
|
|
3
|
+
export class BasePolicy {
|
|
4
|
+
allow(message) {
|
|
5
|
+
return AuthorizationResponse.allow(message);
|
|
6
|
+
}
|
|
7
|
+
deny(message, code) {
|
|
8
|
+
return AuthorizationResponse.deny(message, code);
|
|
9
|
+
}
|
|
10
|
+
denyIf(condition, message) {
|
|
11
|
+
if (condition)
|
|
12
|
+
return this.deny(message);
|
|
13
|
+
return !0;
|
|
14
|
+
}
|
|
15
|
+
denyUnless(condition, message) {
|
|
16
|
+
if (!condition)
|
|
17
|
+
return this.deny(message);
|
|
18
|
+
return !0;
|
|
19
|
+
}
|
|
20
|
+
allowIf(condition, message) {
|
|
21
|
+
if (condition)
|
|
22
|
+
return this.allow(message);
|
|
23
|
+
return !1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
import { log } from "@stacksjs/logging";
|
|
27
|
+
import * as p from "@stacksjs/path";
|
|
28
|
+
import { policy as registerPolicy } from "./gate";
|
|
29
|
+
export async function discoverPolicies() {
|
|
30
|
+
const { fs } = await import("@stacksjs/storage"), policiesDir = p.appPath("Policies");
|
|
31
|
+
if (!fs.existsSync(policiesDir)) {
|
|
32
|
+
log.debug("No Policies directory found");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const mappings = (await import(p.appPath("Gates.ts"))).policies || {};
|
|
37
|
+
for (const [modelName, config] of Object.entries(mappings)) {
|
|
38
|
+
const policyFile = typeof config === "string" ? config : config.policy, policyPath = `${policiesDir}/${policyFile}.ts`;
|
|
39
|
+
if (fs.existsSync(policyPath)) {
|
|
40
|
+
const policyModule = await import(policyPath), PolicyClass = policyModule.default || policyModule[policyFile];
|
|
41
|
+
if (PolicyClass) {
|
|
42
|
+
registerPolicy(modelName, PolicyClass);
|
|
43
|
+
log.debug(`Registered policy: ${policyFile} for ${modelName}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
log.debug("No Gates.ts found, using convention-based discovery");
|
|
49
|
+
}
|
|
50
|
+
const policyFiles = fs.readdirSync(policiesDir).filter((file) => file.endsWith("Policy.ts"));
|
|
51
|
+
for (const file of policyFiles) {
|
|
52
|
+
const policyName = file.replace(".ts", ""), modelName = policyName.replace("Policy", ""), policyPath = `${policiesDir}/${file}`;
|
|
53
|
+
try {
|
|
54
|
+
const policyModule = await import(policyPath), PolicyClass = policyModule.default || policyModule[policyName];
|
|
55
|
+
if (PolicyClass) {
|
|
56
|
+
registerPolicy(modelName, PolicyClass);
|
|
57
|
+
log.debug(`Auto-discovered policy: ${policyName} for ${modelName}`);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
log.error(`Failed to load policy ${policyName}:`, error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function registerGates() {
|
|
65
|
+
const { define, before, after } = await import("./gate");
|
|
66
|
+
try {
|
|
67
|
+
const gatesModule = await import(p.appPath("Gates.ts")), gates = gatesModule.gates || gatesModule.default?.gates || {};
|
|
68
|
+
for (const [ability, callback] of Object.entries(gates))
|
|
69
|
+
if (typeof callback === "function") {
|
|
70
|
+
define(ability, callback);
|
|
71
|
+
log.debug(`Registered gate: ${ability}`);
|
|
72
|
+
}
|
|
73
|
+
const beforeCallbacks = gatesModule.before || gatesModule.default?.before || [];
|
|
74
|
+
for (const callback of beforeCallbacks)
|
|
75
|
+
if (typeof callback === "function")
|
|
76
|
+
before(callback);
|
|
77
|
+
const afterCallbacks = gatesModule.after || gatesModule.default?.after || [];
|
|
78
|
+
for (const callback of afterCallbacks)
|
|
79
|
+
if (typeof callback === "function")
|
|
80
|
+
after(callback);
|
|
81
|
+
log.debug("Gates registered successfully");
|
|
82
|
+
} catch {
|
|
83
|
+
log.debug("No Gates.ts found or failed to load");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function initializeAuthorization() {
|
|
87
|
+
await registerGates();
|
|
88
|
+
await discoverPolicies();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { AuthorizationResponse };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { HttpError } from "@stacksjs/error-handling";
|
|
2
|
+
const MAX_ATTEMPTS = 5, LOCKOUT_DURATION = 900000, MAX_STORE_SIZE = 1e4, EVICTION_INTERVAL = 300000;
|
|
3
|
+
|
|
4
|
+
class MemoryStore {
|
|
5
|
+
store = new Map;
|
|
6
|
+
lastEviction = Date.now();
|
|
7
|
+
evict() {
|
|
8
|
+
const now = Date.now(), intervalElapsed = now - this.lastEviction >= EVICTION_INTERVAL, overCapacity = this.store.size >= MAX_STORE_SIZE;
|
|
9
|
+
if (!intervalElapsed && !overCapacity)
|
|
10
|
+
return;
|
|
11
|
+
this.lastEviction = now;
|
|
12
|
+
for (const [key, value] of this.store)
|
|
13
|
+
if (value.lockedUntil > 0 && value.lockedUntil <= now)
|
|
14
|
+
this.store.delete(key);
|
|
15
|
+
else if (value.lockedUntil === 0 && value.attempts === 0)
|
|
16
|
+
this.store.delete(key);
|
|
17
|
+
}
|
|
18
|
+
get(key) {
|
|
19
|
+
this.evict();
|
|
20
|
+
return this.store.get(key);
|
|
21
|
+
}
|
|
22
|
+
set(key, entry) {
|
|
23
|
+
this.store.set(key, entry);
|
|
24
|
+
}
|
|
25
|
+
delete(key) {
|
|
26
|
+
this.store.delete(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class CacheStore {
|
|
31
|
+
prefix = "auth:ratelimit:";
|
|
32
|
+
async get(key) {
|
|
33
|
+
const { cache } = await import("@stacksjs/cache"), raw = await cache.get(`${this.prefix}${key}`);
|
|
34
|
+
if (raw == null)
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async set(key, entry, ttlMs) {
|
|
43
|
+
const { cache } = await import("@stacksjs/cache");
|
|
44
|
+
await cache.set(`${this.prefix}${key}`, JSON.stringify(entry), Math.ceil(ttlMs / 1000));
|
|
45
|
+
}
|
|
46
|
+
async delete(key) {
|
|
47
|
+
const { cache } = await import("@stacksjs/cache");
|
|
48
|
+
await cache.remove(`${this.prefix}${key}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let store = new MemoryStore;
|
|
52
|
+
|
|
53
|
+
export class RateLimiter {
|
|
54
|
+
static useStore(custom) {
|
|
55
|
+
store = custom;
|
|
56
|
+
}
|
|
57
|
+
static useSharedStore() {
|
|
58
|
+
store = new CacheStore;
|
|
59
|
+
}
|
|
60
|
+
static useMemoryStore() {
|
|
61
|
+
store = new MemoryStore;
|
|
62
|
+
}
|
|
63
|
+
static async isRateLimited(email) {
|
|
64
|
+
email = email.toLowerCase();
|
|
65
|
+
const now = Date.now(), userAttempts = await store.get(email);
|
|
66
|
+
if (!userAttempts)
|
|
67
|
+
return !1;
|
|
68
|
+
if (userAttempts.lockedUntil > 0 && userAttempts.lockedUntil <= now) {
|
|
69
|
+
await store.delete(email);
|
|
70
|
+
return !1;
|
|
71
|
+
}
|
|
72
|
+
return userAttempts.lockedUntil > 0;
|
|
73
|
+
}
|
|
74
|
+
static async recordFailedAttempt(email) {
|
|
75
|
+
email = email.toLowerCase();
|
|
76
|
+
const now = Date.now(), userAttempts = await store.get(email) || { attempts: 0, lockedUntil: 0 };
|
|
77
|
+
userAttempts.attempts++;
|
|
78
|
+
if (userAttempts.attempts >= MAX_ATTEMPTS) {
|
|
79
|
+
userAttempts.lockedUntil = now + LOCKOUT_DURATION;
|
|
80
|
+
userAttempts.attempts = 0;
|
|
81
|
+
}
|
|
82
|
+
await store.set(email, userAttempts, LOCKOUT_DURATION);
|
|
83
|
+
}
|
|
84
|
+
static async resetAttempts(email) {
|
|
85
|
+
await store.delete(email.toLowerCase());
|
|
86
|
+
}
|
|
87
|
+
static async validateAttempt(email) {
|
|
88
|
+
if (await this.isRateLimited(email))
|
|
89
|
+
throw new HttpError(429, "Too many login attempts. Please try again later.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createRole, findRole } from "./rbac";
|
|
2
|
+
export const DEFAULT_ROLE_PACKS = [
|
|
3
|
+
{
|
|
4
|
+
name: "admin",
|
|
5
|
+
guard_name: "web",
|
|
6
|
+
description: "Full access. Sees every dashboard surface, every model, every infra control."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: "dev",
|
|
10
|
+
guard_name: "web",
|
|
11
|
+
description: "Developer / infra. Sees dev-mode surfaces (CI, query inspector, runner alerts) but not billing/admin-only management."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "client",
|
|
15
|
+
guard_name: "web",
|
|
16
|
+
description: "End user / client. Sees content, orders, profile, billing \u2014 no dev tools, no infra surfaces."
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
export async function seedDefaultRoles() {
|
|
20
|
+
const created = [], skipped = [];
|
|
21
|
+
for (const pack of DEFAULT_ROLE_PACKS) {
|
|
22
|
+
if (await findRole(pack.name, pack.guard_name)) {
|
|
23
|
+
skipped.push({ name: pack.name, guard_name: pack.guard_name, reason: "already_exists" });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const role = await createRole(pack.name, pack.guard_name, pack.description);
|
|
27
|
+
created.push(role);
|
|
28
|
+
}
|
|
29
|
+
return { created, skipped };
|
|
30
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { db } from "@stacksjs/database";
|
|
2
|
+
import { isUniqueViolation } from "@stacksjs/orm";
|
|
3
|
+
|
|
4
|
+
export { isUniqueViolation };
|
|
5
|
+
export function swallowDuplicate(err) {
|
|
6
|
+
if (!isUniqueViolation(err))
|
|
7
|
+
throw err;
|
|
8
|
+
}
|
|
9
|
+
export function toRecord(row) {
|
|
10
|
+
if (!row)
|
|
11
|
+
return null;
|
|
12
|
+
return {
|
|
13
|
+
id: Number(row.id),
|
|
14
|
+
name: String(row.name),
|
|
15
|
+
guard_name: String(row.guard_name),
|
|
16
|
+
description: row.description == null ? void 0 : String(row.description),
|
|
17
|
+
created_at: row.created_at == null ? void 0 : String(row.created_at),
|
|
18
|
+
updated_at: row.updated_at == null ? void 0 : String(row.updated_at)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function insertAndFetch(table, name, guardName, description) {
|
|
22
|
+
await db.insertInto(table).values({ name, guard_name: guardName, description: description ?? null }).execute();
|
|
23
|
+
const row = await db.selectFrom(table).selectAll().where("name", "=", name).where("guard_name", "=", guardName).orderBy("id", "desc").limit(1).executeTakeFirst(), rec = toRecord(row);
|
|
24
|
+
if (!rec)
|
|
25
|
+
throw Error(`[rbac] insertAndFetch(${table}, ${name}, ${guardName}) succeeded but follow-up SELECT returned nothing`);
|
|
26
|
+
return rec;
|
|
27
|
+
}
|
|
28
|
+
async function pivotAttach(table, cols) {
|
|
29
|
+
let q = db.selectFrom(table).select("user_id" in cols ? "user_id" : "role_id");
|
|
30
|
+
for (const [k, v] of Object.entries(cols))
|
|
31
|
+
q = q.where(k, "=", v);
|
|
32
|
+
if (await q.limit(1).executeTakeFirst())
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
await db.insertInto(table).values(cols).execute();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
swallowDuplicate(err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function pivotDetach(table, cols) {
|
|
41
|
+
let q = db.deleteFrom(table);
|
|
42
|
+
for (const [k, v] of Object.entries(cols))
|
|
43
|
+
q = q.where(k, "=", v);
|
|
44
|
+
await q.execute();
|
|
45
|
+
}
|
|
46
|
+
async function pivotDetachAll(table, column, value) {
|
|
47
|
+
await db.deleteFrom(table).where(column, "=", value).execute();
|
|
48
|
+
}
|
|
49
|
+
async function pivotSync(table, ownerColumn, ownerId, targetColumn, targetIds) {
|
|
50
|
+
const unique = Array.from(new Set(targetIds));
|
|
51
|
+
await db.transaction(async (rawTrx) => {
|
|
52
|
+
const trx = rawTrx;
|
|
53
|
+
await trx.deleteFrom(table).where(ownerColumn, "=", ownerId).execute();
|
|
54
|
+
if (unique.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const rows = unique.map((id) => ({ [ownerColumn]: ownerId, [targetColumn]: id }));
|
|
57
|
+
try {
|
|
58
|
+
await trx.insertInto(table).values(rows).execute();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
swallowDuplicate(err);
|
|
61
|
+
for (const id of unique)
|
|
62
|
+
try {
|
|
63
|
+
await trx.insertInto(table).values({ [ownerColumn]: ownerId, [targetColumn]: id }).execute();
|
|
64
|
+
} catch (innerErr) {
|
|
65
|
+
swallowDuplicate(innerErr);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function createBqbRbacStore() {
|
|
71
|
+
return {
|
|
72
|
+
async findRoleByName(name, guardName = "web") {
|
|
73
|
+
const row = await db.selectFrom("roles").selectAll().where("name", "=", name).where("guard_name", "=", guardName).limit(1).executeTakeFirst();
|
|
74
|
+
return toRecord(row);
|
|
75
|
+
},
|
|
76
|
+
async findRoleById(id) {
|
|
77
|
+
const row = await db.selectFrom("roles").selectAll().where("id", "=", id).limit(1).executeTakeFirst();
|
|
78
|
+
return toRecord(row);
|
|
79
|
+
},
|
|
80
|
+
async createRole(name, guardName = "web", description) {
|
|
81
|
+
return await insertAndFetch("roles", name, guardName, description);
|
|
82
|
+
},
|
|
83
|
+
async deleteRole(id) {
|
|
84
|
+
await db.deleteFrom("user_roles").where("role_id", "=", id).execute();
|
|
85
|
+
await db.deleteFrom("role_permissions").where("role_id", "=", id).execute();
|
|
86
|
+
await db.deleteFrom("roles").where("id", "=", id).execute();
|
|
87
|
+
},
|
|
88
|
+
async getAllRoles(guardName) {
|
|
89
|
+
let q = db.selectFrom("roles").selectAll();
|
|
90
|
+
if (guardName)
|
|
91
|
+
q = q.where("guard_name", "=", guardName);
|
|
92
|
+
return (await q.orderBy("id", "asc").execute()).map((r) => toRecord(r)).filter(Boolean);
|
|
93
|
+
},
|
|
94
|
+
async findPermissionByName(name, guardName = "web") {
|
|
95
|
+
const row = await db.selectFrom("permissions").selectAll().where("name", "=", name).where("guard_name", "=", guardName).limit(1).executeTakeFirst();
|
|
96
|
+
return toRecord(row);
|
|
97
|
+
},
|
|
98
|
+
async findPermissionById(id) {
|
|
99
|
+
const row = await db.selectFrom("permissions").selectAll().where("id", "=", id).limit(1).executeTakeFirst();
|
|
100
|
+
return toRecord(row);
|
|
101
|
+
},
|
|
102
|
+
async createPermission(name, guardName = "web", description) {
|
|
103
|
+
return await insertAndFetch("permissions", name, guardName, description);
|
|
104
|
+
},
|
|
105
|
+
async deletePermission(id) {
|
|
106
|
+
await db.deleteFrom("user_permissions").where("permission_id", "=", id).execute();
|
|
107
|
+
await db.deleteFrom("role_permissions").where("permission_id", "=", id).execute();
|
|
108
|
+
await db.deleteFrom("permissions").where("id", "=", id).execute();
|
|
109
|
+
},
|
|
110
|
+
async getAllPermissions(guardName) {
|
|
111
|
+
let q = db.selectFrom("permissions").selectAll();
|
|
112
|
+
if (guardName)
|
|
113
|
+
q = q.where("guard_name", "=", guardName);
|
|
114
|
+
return (await q.orderBy("id", "asc").execute()).map((r) => toRecord(r)).filter(Boolean);
|
|
115
|
+
},
|
|
116
|
+
async getUserRoles(userId) {
|
|
117
|
+
return (await db.selectFrom("roles").innerJoin("user_roles", "user_roles.role_id", "=", "roles.id").select([
|
|
118
|
+
"roles.id as id",
|
|
119
|
+
"roles.name as name",
|
|
120
|
+
"roles.guard_name as guard_name",
|
|
121
|
+
"roles.description as description",
|
|
122
|
+
"roles.created_at as created_at",
|
|
123
|
+
"roles.updated_at as updated_at"
|
|
124
|
+
]).where("user_roles.user_id", "=", userId).orderBy("roles.id", "asc").execute()).map((r) => toRecord(r)).filter(Boolean);
|
|
125
|
+
},
|
|
126
|
+
assignRoleToUser(userId, roleId) {
|
|
127
|
+
return pivotAttach("user_roles", { user_id: userId, role_id: roleId });
|
|
128
|
+
},
|
|
129
|
+
removeRoleFromUser(userId, roleId) {
|
|
130
|
+
return pivotDetach("user_roles", { user_id: userId, role_id: roleId });
|
|
131
|
+
},
|
|
132
|
+
removeAllRolesFromUser(userId) {
|
|
133
|
+
return pivotDetachAll("user_roles", "user_id", userId);
|
|
134
|
+
},
|
|
135
|
+
syncUserRoles(userId, roleIds) {
|
|
136
|
+
return pivotSync("user_roles", "user_id", userId, "role_id", roleIds);
|
|
137
|
+
},
|
|
138
|
+
async getUserDirectPermissions(userId) {
|
|
139
|
+
return (await db.selectFrom("permissions").innerJoin("user_permissions", "user_permissions.permission_id", "=", "permissions.id").select([
|
|
140
|
+
"permissions.id as id",
|
|
141
|
+
"permissions.name as name",
|
|
142
|
+
"permissions.guard_name as guard_name",
|
|
143
|
+
"permissions.description as description",
|
|
144
|
+
"permissions.created_at as created_at",
|
|
145
|
+
"permissions.updated_at as updated_at"
|
|
146
|
+
]).where("user_permissions.user_id", "=", userId).orderBy("permissions.id", "asc").execute()).map((r) => toRecord(r)).filter(Boolean);
|
|
147
|
+
},
|
|
148
|
+
assignPermissionToUser(userId, permissionId) {
|
|
149
|
+
return pivotAttach("user_permissions", { user_id: userId, permission_id: permissionId });
|
|
150
|
+
},
|
|
151
|
+
removePermissionFromUser(userId, permissionId) {
|
|
152
|
+
return pivotDetach("user_permissions", { user_id: userId, permission_id: permissionId });
|
|
153
|
+
},
|
|
154
|
+
removeAllPermissionsFromUser(userId) {
|
|
155
|
+
return pivotDetachAll("user_permissions", "user_id", userId);
|
|
156
|
+
},
|
|
157
|
+
syncUserPermissions(userId, permissionIds) {
|
|
158
|
+
return pivotSync("user_permissions", "user_id", userId, "permission_id", permissionIds);
|
|
159
|
+
},
|
|
160
|
+
async getRolePermissions(roleId) {
|
|
161
|
+
return (await db.selectFrom("permissions").innerJoin("role_permissions", "role_permissions.permission_id", "=", "permissions.id").select([
|
|
162
|
+
"permissions.id as id",
|
|
163
|
+
"permissions.name as name",
|
|
164
|
+
"permissions.guard_name as guard_name",
|
|
165
|
+
"permissions.description as description",
|
|
166
|
+
"permissions.created_at as created_at",
|
|
167
|
+
"permissions.updated_at as updated_at"
|
|
168
|
+
]).where("role_permissions.role_id", "=", roleId).orderBy("permissions.id", "asc").execute()).map((r) => toRecord(r)).filter(Boolean);
|
|
169
|
+
},
|
|
170
|
+
assignPermissionToRole(roleId, permissionId) {
|
|
171
|
+
return pivotAttach("role_permissions", { role_id: roleId, permission_id: permissionId });
|
|
172
|
+
},
|
|
173
|
+
removePermissionFromRole(roleId, permissionId) {
|
|
174
|
+
return pivotDetach("role_permissions", { role_id: roleId, permission_id: permissionId });
|
|
175
|
+
},
|
|
176
|
+
syncRolePermissions(roleId, permissionIds) {
|
|
177
|
+
return pivotSync("role_permissions", "role_id", roleId, "permission_id", permissionIds);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|