@lastshotlabs/bunshot 0.0.9 → 0.0.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/README.md +70 -3
- package/dist/adapters/memoryAuth.d.ts +5 -0
- package/dist/adapters/memoryAuth.js +23 -0
- package/dist/adapters/sqliteAuth.d.ts +5 -0
- package/dist/adapters/sqliteAuth.js +18 -0
- package/dist/app.d.ts +18 -2
- package/dist/app.js +18 -5
- package/dist/entrypoints/mongo.d.ts +3 -0
- package/dist/entrypoints/mongo.js +3 -0
- package/dist/entrypoints/queue.d.ts +2 -0
- package/dist/entrypoints/queue.js +1 -0
- package/dist/entrypoints/redis.d.ts +1 -0
- package/dist/entrypoints/redis.js +1 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +4 -5
- package/dist/lib/appConfig.d.ts +9 -0
- package/dist/lib/appConfig.js +5 -0
- package/dist/lib/emailVerification.js +11 -10
- package/dist/lib/mongo.d.ts +9 -4
- package/dist/lib/mongo.js +61 -10
- package/dist/lib/oauth.js +11 -10
- package/dist/lib/queue.d.ts +3 -4
- package/dist/lib/queue.js +18 -3
- package/dist/lib/redis.d.ts +3 -8
- package/dist/lib/redis.js +19 -8
- package/dist/lib/resetPassword.d.ts +12 -0
- package/dist/lib/resetPassword.js +95 -0
- package/dist/lib/session.js +12 -12
- package/dist/middleware/cacheResponse.js +10 -9
- package/dist/models/AuthUser.d.ts +14 -106
- package/dist/models/AuthUser.js +31 -14
- package/dist/routes/auth.d.ts +3 -2
- package/dist/routes/auth.js +76 -1
- package/package.json +38 -8
package/dist/lib/mongo.js
CHANGED
|
@@ -1,32 +1,74 @@
|
|
|
1
|
-
import mongoose from "mongoose";
|
|
2
1
|
import { log } from "./logger";
|
|
3
2
|
const isProd = process.env.NODE_ENV === "production";
|
|
3
|
+
function requireMongoose() {
|
|
4
|
+
try {
|
|
5
|
+
// Bun supports require() in ESM; this defers the import to call time
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
7
|
+
const mod = require("mongoose");
|
|
8
|
+
return mod.default ?? mod;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error("mongoose is not installed. Run: bun add mongoose");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
function buildUri(user, password, host, db) {
|
|
5
15
|
const [hostPart, queryPart] = host.split("?");
|
|
6
16
|
return `mongodb+srv://${user}:${password}@${hostPart.replace(/\/$/, "")}/${db}${queryPart ? `?${queryPart}` : ""}`;
|
|
7
17
|
}
|
|
18
|
+
// Internal mutable references — set inside connect functions
|
|
19
|
+
let _authConn = null;
|
|
20
|
+
let _appConn = null;
|
|
21
|
+
let _mongoose = null;
|
|
22
|
+
function makeConnectionProxy(label, getConn, setConn) {
|
|
23
|
+
return new Proxy({}, {
|
|
24
|
+
get(_, prop) {
|
|
25
|
+
let conn = getConn();
|
|
26
|
+
if (!conn) {
|
|
27
|
+
// Lazily create a disconnected connection so appConnection.model() works at module
|
|
28
|
+
// load time. Mongoose buffers queries until openUri() is called by connectMongo().
|
|
29
|
+
conn = requireMongoose().createConnection();
|
|
30
|
+
setConn(conn);
|
|
31
|
+
}
|
|
32
|
+
const val = conn[prop];
|
|
33
|
+
return typeof val === "function" ? val.bind(conn) : val;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
8
37
|
/**
|
|
9
38
|
* Named connection used exclusively for auth data (AuthUser model).
|
|
10
39
|
* Connected via connectAuthMongo() or connectMongo() (backward compat).
|
|
11
40
|
*/
|
|
12
|
-
export const authConnection =
|
|
41
|
+
export const authConnection = makeConnectionProxy("auth", () => _authConn, (c) => { _authConn = c; });
|
|
13
42
|
/**
|
|
14
43
|
* Named connection for app/tenant data.
|
|
15
44
|
* Connected via connectAppMongo() or connectMongo() (backward compat).
|
|
16
45
|
* Use this when registering your own models: appConnection.model("Product", schema).
|
|
17
46
|
*/
|
|
18
|
-
export const appConnection =
|
|
47
|
+
export const appConnection = makeConnectionProxy("app", () => _appConn, (c) => { _appConn = c; });
|
|
48
|
+
/**
|
|
49
|
+
* The mongoose instance. Available after connectMongo() / connectAuthMongo() is called.
|
|
50
|
+
*/
|
|
51
|
+
export const mongoose = new Proxy({}, {
|
|
52
|
+
get(_, prop) {
|
|
53
|
+
const mg = _mongoose ?? requireMongoose();
|
|
54
|
+
return mg[prop];
|
|
55
|
+
},
|
|
56
|
+
});
|
|
19
57
|
/**
|
|
20
58
|
* Connect the auth connection to its dedicated MongoDB server.
|
|
21
59
|
* Uses MONGO_AUTH_USER_*, MONGO_AUTH_PW_*, MONGO_AUTH_HOST_*, MONGO_AUTH_DB_* env vars.
|
|
22
60
|
*/
|
|
23
61
|
export const connectAuthMongo = async () => {
|
|
62
|
+
const mg = requireMongoose();
|
|
63
|
+
_mongoose = mg;
|
|
64
|
+
if (!_authConn)
|
|
65
|
+
_authConn = mg.createConnection();
|
|
24
66
|
const user = isProd ? process.env.MONGO_AUTH_USER_PROD : process.env.MONGO_AUTH_USER_DEV;
|
|
25
67
|
const password = isProd ? process.env.MONGO_AUTH_PW_PROD : process.env.MONGO_AUTH_PW_DEV;
|
|
26
68
|
const host = isProd ? process.env.MONGO_AUTH_HOST_PROD : process.env.MONGO_AUTH_HOST_DEV;
|
|
27
69
|
const db = isProd ? process.env.MONGO_AUTH_DB_PROD : process.env.MONGO_AUTH_DB_DEV;
|
|
28
70
|
const uri = buildUri(user, password, host, db);
|
|
29
|
-
await
|
|
71
|
+
await _authConn.openUri(uri);
|
|
30
72
|
log(`[mongo] auth connected to ${host} as ${user}`);
|
|
31
73
|
};
|
|
32
74
|
/**
|
|
@@ -34,12 +76,16 @@ export const connectAuthMongo = async () => {
|
|
|
34
76
|
* Uses MONGO_USER_*, MONGO_PW_*, MONGO_HOST_*, MONGO_DB_* env vars.
|
|
35
77
|
*/
|
|
36
78
|
export const connectAppMongo = async () => {
|
|
79
|
+
const mg = requireMongoose();
|
|
80
|
+
_mongoose = mg;
|
|
81
|
+
if (!_appConn)
|
|
82
|
+
_appConn = mg.createConnection();
|
|
37
83
|
const user = isProd ? process.env.MONGO_USER_PROD : process.env.MONGO_USER_DEV;
|
|
38
84
|
const password = isProd ? process.env.MONGO_PW_PROD : process.env.MONGO_PW_DEV;
|
|
39
85
|
const host = isProd ? process.env.MONGO_HOST_PROD : process.env.MONGO_HOST_DEV;
|
|
40
86
|
const db = isProd ? process.env.MONGO_DB_PROD : process.env.MONGO_DB_DEV;
|
|
41
87
|
const uri = buildUri(user, password, host, db);
|
|
42
|
-
await
|
|
88
|
+
await _appConn.openUri(uri);
|
|
43
89
|
log(`[mongo] app connected to ${host} as ${user}`);
|
|
44
90
|
};
|
|
45
91
|
/**
|
|
@@ -48,14 +94,20 @@ export const connectAppMongo = async () => {
|
|
|
48
94
|
* Uses MONGO_USER_*, MONGO_PW_*, MONGO_HOST_*, MONGO_DB_* env vars.
|
|
49
95
|
*/
|
|
50
96
|
export const connectMongo = async () => {
|
|
97
|
+
const mg = requireMongoose();
|
|
98
|
+
_mongoose = mg;
|
|
99
|
+
if (!_authConn)
|
|
100
|
+
_authConn = mg.createConnection();
|
|
101
|
+
if (!_appConn)
|
|
102
|
+
_appConn = mg.createConnection();
|
|
51
103
|
const user = isProd ? process.env.MONGO_USER_PROD : process.env.MONGO_USER_DEV;
|
|
52
104
|
const password = isProd ? process.env.MONGO_PW_PROD : process.env.MONGO_PW_DEV;
|
|
53
105
|
const host = isProd ? process.env.MONGO_HOST_PROD : process.env.MONGO_HOST_DEV;
|
|
54
106
|
const db = isProd ? process.env.MONGO_DB_PROD : process.env.MONGO_DB_DEV;
|
|
55
107
|
const uri = buildUri(user, password, host, db);
|
|
56
108
|
await Promise.all([
|
|
57
|
-
|
|
58
|
-
|
|
109
|
+
_authConn.openUri(uri),
|
|
110
|
+
_appConn.openUri(uri),
|
|
59
111
|
]);
|
|
60
112
|
log(`[mongo] connected to ${host} as ${user}`);
|
|
61
113
|
};
|
|
@@ -65,9 +117,8 @@ export const connectMongo = async () => {
|
|
|
65
117
|
*/
|
|
66
118
|
export const disconnectMongo = async () => {
|
|
67
119
|
await Promise.all([
|
|
68
|
-
|
|
69
|
-
|
|
120
|
+
_authConn && _authConn.readyState !== 0 ? _authConn.close() : Promise.resolve(),
|
|
121
|
+
_appConn && _appConn.readyState !== 0 ? _appConn.close() : Promise.resolve(),
|
|
70
122
|
]);
|
|
71
123
|
log("[mongo] disconnected");
|
|
72
124
|
};
|
|
73
|
-
export { mongoose };
|
package/dist/lib/oauth.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Google, Apple, generateState, generateCodeVerifier } from "arctic";
|
|
2
2
|
import { getRedis } from "./redis";
|
|
3
|
-
import { appConnection } from "./mongo";
|
|
3
|
+
import { appConnection, mongoose } from "./mongo";
|
|
4
4
|
import { getAppName } from "./appConfig";
|
|
5
|
-
import { Schema } from "mongoose";
|
|
6
5
|
import { sqliteStoreOAuthState, sqliteConsumeOAuthState } from "../adapters/sqliteAuth";
|
|
7
6
|
import { memoryStoreOAuthState, memoryConsumeOAuthState } from "../adapters/memoryAuth";
|
|
8
7
|
let _providers = {};
|
|
@@ -29,15 +28,17 @@ export const getApple = () => {
|
|
|
29
28
|
export const getConfiguredOAuthProviders = () => Object.entries(_providers)
|
|
30
29
|
.filter(([, v]) => v != null)
|
|
31
30
|
.map(([k]) => k);
|
|
32
|
-
const oauthStateSchema = new Schema({
|
|
33
|
-
state: { type: String, required: true, unique: true },
|
|
34
|
-
codeVerifier: { type: String },
|
|
35
|
-
linkUserId: { type: String },
|
|
36
|
-
expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
|
|
37
|
-
}, { collection: "oauth_states" });
|
|
38
31
|
function getOAuthStateModel() {
|
|
39
|
-
|
|
40
|
-
appConnection.
|
|
32
|
+
if (appConnection.models["OAuthState"])
|
|
33
|
+
return appConnection.models["OAuthState"];
|
|
34
|
+
const { Schema } = mongoose;
|
|
35
|
+
const oauthStateSchema = new Schema({
|
|
36
|
+
state: { type: String, required: true, unique: true },
|
|
37
|
+
codeVerifier: { type: String },
|
|
38
|
+
linkUserId: { type: String },
|
|
39
|
+
expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
|
|
40
|
+
}, { collection: "oauth_states" });
|
|
41
|
+
return appConnection.model("OAuthState", oauthStateSchema);
|
|
41
42
|
}
|
|
42
43
|
let _oauthStore = "redis";
|
|
43
44
|
export const setOAuthStateStore = (store) => { _oauthStore = store; };
|
package/dist/lib/queue.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Queue, Worker } from "bullmq";
|
|
2
|
-
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const createWorker: <T = unknown, R = unknown>(name: string, processor: Processor<T, R>, options?: Omit<WorkerOptions, "connection">) => Worker<T, R>;
|
|
1
|
+
import type { Queue as QueueType, Worker as WorkerType, Processor, QueueOptions, WorkerOptions, Job } from "bullmq";
|
|
2
|
+
export declare const createQueue: <T = unknown, R = unknown>(name: string, options?: Omit<QueueOptions, "connection">) => QueueType<T, R>;
|
|
3
|
+
export declare const createWorker: <T = unknown, R = unknown>(name: string, processor: Processor<T, R>, options?: Omit<WorkerOptions, "connection">) => WorkerType<T, R>;
|
|
5
4
|
export type { Job };
|
package/dist/lib/queue.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { Queue, Worker } from "bullmq";
|
|
2
1
|
import { getRedisConnectionOptions } from "./redis";
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
function requireBullMQ() {
|
|
3
|
+
try {
|
|
4
|
+
// Bun supports require() in ESM; this defers the import to call time
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
6
|
+
return require("bullmq");
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
throw new Error("bullmq is not installed. Run: bun add bullmq");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const createQueue = (name, options) => {
|
|
13
|
+
const { Queue } = requireBullMQ();
|
|
14
|
+
return new Queue(name, { connection: getRedisConnectionOptions(), ...options });
|
|
15
|
+
};
|
|
16
|
+
export const createWorker = (name, processor, options) => {
|
|
17
|
+
const { Worker } = requireBullMQ();
|
|
18
|
+
return new Worker(name, processor, { connection: getRedisConnectionOptions(), ...options });
|
|
19
|
+
};
|
package/dist/lib/redis.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
export declare const getRedisConnectionOptions: () =>
|
|
3
|
-
password?: string | undefined;
|
|
4
|
-
username?: string | undefined;
|
|
5
|
-
host: string;
|
|
6
|
-
port: number;
|
|
7
|
-
};
|
|
1
|
+
import type { default as RedisClass, RedisOptions } from "ioredis";
|
|
2
|
+
export declare const getRedisConnectionOptions: () => RedisOptions;
|
|
8
3
|
export declare const connectRedis: () => Promise<void>;
|
|
9
4
|
/**
|
|
10
5
|
* Gracefully close the Redis connection.
|
|
11
6
|
* Useful for one-off scripts that need a clean exit.
|
|
12
7
|
*/
|
|
13
8
|
export declare const disconnectRedis: () => Promise<void>;
|
|
14
|
-
export declare const getRedis: () =>
|
|
9
|
+
export declare const getRedis: () => RedisClass;
|
package/dist/lib/redis.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import Redis from "ioredis";
|
|
2
1
|
import { log } from "./logger";
|
|
3
2
|
const isProd = process.env.NODE_ENV === "production";
|
|
3
|
+
function requireIoredis() {
|
|
4
|
+
try {
|
|
5
|
+
// Bun supports require() in ESM; this defers the import to call time
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
7
|
+
const mod = require("ioredis");
|
|
8
|
+
return mod.default ?? mod;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error("ioredis is not installed. Run: bun add ioredis");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
export const getRedisConnectionOptions = () => {
|
|
5
15
|
const host_port = isProd ? process.env.REDIS_HOST_PROD : process.env.REDIS_HOST_DEV;
|
|
6
16
|
if (!host_port)
|
|
@@ -18,17 +28,18 @@ export const getRedisConnectionOptions = () => {
|
|
|
18
28
|
};
|
|
19
29
|
};
|
|
20
30
|
let client = null;
|
|
21
|
-
const createClient = () => {
|
|
22
|
-
const redis = new Redis(getRedisConnectionOptions());
|
|
23
|
-
redis.on("error", (err) => log(`[redis] error: ${err.message}`));
|
|
24
|
-
return redis;
|
|
25
|
-
};
|
|
26
31
|
export const connectRedis = () => {
|
|
27
32
|
if (client)
|
|
28
33
|
return Promise.resolve();
|
|
29
|
-
|
|
34
|
+
const Redis = requireIoredis();
|
|
35
|
+
client = new Redis(getRedisConnectionOptions());
|
|
36
|
+
client.on("error", (err) => log(`[redis] error: ${err.message}`));
|
|
30
37
|
return new Promise((resolve, reject) => {
|
|
31
|
-
client.once("ready", () => {
|
|
38
|
+
client.once("ready", () => {
|
|
39
|
+
const opts = getRedisConnectionOptions();
|
|
40
|
+
log(`[redis] connected to ${opts.host}:${opts.port} as ${opts.username || "default user"}`);
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
32
43
|
client.once("error", reject);
|
|
33
44
|
});
|
|
34
45
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type ResetStore = "redis" | "mongo" | "sqlite" | "memory";
|
|
2
|
+
export declare const setPasswordResetStore: (store: ResetStore) => void;
|
|
3
|
+
/** Create a reset token. Returns the raw token (to embed in the email link).
|
|
4
|
+
* Only the SHA-256 hash is persisted in the store. */
|
|
5
|
+
export declare const createResetToken: (userId: string, email: string) => Promise<string>;
|
|
6
|
+
/** Atomically consume a reset token — returns its payload and deletes it in one operation.
|
|
7
|
+
* Returns null if the token is invalid, expired, or already used. */
|
|
8
|
+
export declare const consumeResetToken: (token: string) => Promise<{
|
|
9
|
+
userId: string;
|
|
10
|
+
email: string;
|
|
11
|
+
} | null>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { getRedis } from "./redis";
|
|
3
|
+
import { appConnection } from "./mongo";
|
|
4
|
+
import { getAppName, getResetTokenExpiry } from "./appConfig";
|
|
5
|
+
import { Schema } from "mongoose";
|
|
6
|
+
import { sqliteCreateResetToken, sqliteConsumeResetToken, } from "../adapters/sqliteAuth";
|
|
7
|
+
import { memoryCreateResetToken, memoryConsumeResetToken, } from "../adapters/memoryAuth";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Token hashing — store SHA-256(token); raw token is only in the email link.
|
|
10
|
+
// If the store is ever leaked, outstanding tokens cannot be replayed directly.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const hashToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
13
|
+
const resetSchema = new Schema({
|
|
14
|
+
token: { type: String, required: true, unique: true },
|
|
15
|
+
userId: { type: String, required: true },
|
|
16
|
+
email: { type: String, required: true },
|
|
17
|
+
expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
|
|
18
|
+
}, { collection: "password_resets" });
|
|
19
|
+
function getResetModel() {
|
|
20
|
+
return appConnection.models["PasswordReset"] ??
|
|
21
|
+
appConnection.model("PasswordReset", resetSchema);
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Redis helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** Atomically GET+DEL a key. Uses native GETDEL (Redis >= 6.2) with a Lua fallback. */
|
|
27
|
+
async function redisGetDel(key) {
|
|
28
|
+
const redis = getRedis();
|
|
29
|
+
if (typeof redis.getdel === "function") {
|
|
30
|
+
try {
|
|
31
|
+
return await redis.getdel(key);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const msg = err?.message ?? "";
|
|
35
|
+
if (!/unknown command|ERR unknown command/i.test(msg))
|
|
36
|
+
throw err;
|
|
37
|
+
// Fall through to Lua on "unknown command"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const result = await redis.eval("local v = redis.call('GET', KEYS[1])\nif v then redis.call('DEL', KEYS[1]) end\nreturn v", 1, key);
|
|
41
|
+
return result ?? null;
|
|
42
|
+
}
|
|
43
|
+
let _store = "redis";
|
|
44
|
+
export const setPasswordResetStore = (store) => { _store = store; };
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Public API
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/** Create a reset token. Returns the raw token (to embed in the email link).
|
|
49
|
+
* Only the SHA-256 hash is persisted in the store. */
|
|
50
|
+
export const createResetToken = async (userId, email) => {
|
|
51
|
+
const token = crypto.randomUUID();
|
|
52
|
+
const hash = hashToken(token);
|
|
53
|
+
const ttl = getResetTokenExpiry();
|
|
54
|
+
if (_store === "memory") {
|
|
55
|
+
memoryCreateResetToken(hash, userId, email, ttl);
|
|
56
|
+
return token;
|
|
57
|
+
}
|
|
58
|
+
if (_store === "sqlite") {
|
|
59
|
+
sqliteCreateResetToken(hash, userId, email, ttl);
|
|
60
|
+
return token;
|
|
61
|
+
}
|
|
62
|
+
if (_store === "mongo") {
|
|
63
|
+
await getResetModel().create({
|
|
64
|
+
token: hash,
|
|
65
|
+
userId,
|
|
66
|
+
email,
|
|
67
|
+
expiresAt: new Date(Date.now() + ttl * 1000),
|
|
68
|
+
});
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
71
|
+
await getRedis().set(`reset:${getAppName()}:${hash}`, JSON.stringify({ userId, email }), "EX", ttl);
|
|
72
|
+
return token;
|
|
73
|
+
};
|
|
74
|
+
/** Atomically consume a reset token — returns its payload and deletes it in one operation.
|
|
75
|
+
* Returns null if the token is invalid, expired, or already used. */
|
|
76
|
+
export const consumeResetToken = async (token) => {
|
|
77
|
+
const hash = hashToken(token);
|
|
78
|
+
if (_store === "memory")
|
|
79
|
+
return memoryConsumeResetToken(hash);
|
|
80
|
+
if (_store === "sqlite")
|
|
81
|
+
return sqliteConsumeResetToken(hash);
|
|
82
|
+
if (_store === "mongo") {
|
|
83
|
+
const doc = await getResetModel()
|
|
84
|
+
.findOneAndDelete({ token: hash, expiresAt: { $gt: new Date() } })
|
|
85
|
+
.lean();
|
|
86
|
+
if (!doc)
|
|
87
|
+
return null;
|
|
88
|
+
return { userId: doc.userId, email: doc.email };
|
|
89
|
+
}
|
|
90
|
+
// Redis: atomically return and remove the key (GETDEL or Lua fallback)
|
|
91
|
+
const raw = await redisGetDel(`reset:${getAppName()}:${hash}`);
|
|
92
|
+
if (!raw)
|
|
93
|
+
return null;
|
|
94
|
+
return JSON.parse(raw);
|
|
95
|
+
};
|
package/dist/lib/session.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { getRedis } from "./redis";
|
|
2
|
-
import { appConnection } from "./mongo";
|
|
2
|
+
import { appConnection, mongoose } from "./mongo";
|
|
3
3
|
import { getAppName, getPersistSessionMetadata, getIncludeInactiveSessions } from "./appConfig";
|
|
4
|
-
import { Schema } from "mongoose";
|
|
5
4
|
import { sqliteCreateSession, sqliteGetSession, sqliteDeleteSession, sqliteGetUserSessions, sqliteGetActiveSessionCount, sqliteEvictOldestSession, sqliteUpdateSessionLastActive, } from "../adapters/sqliteAuth";
|
|
6
5
|
import { memoryCreateSession, memoryGetSession, memoryDeleteSession, memoryGetUserSessions, memoryGetActiveSessionCount, memoryEvictOldestSession, memoryUpdateSessionLastActive, } from "../adapters/memoryAuth";
|
|
7
|
-
const sessionSchema = new Schema({
|
|
8
|
-
sessionId: { type: String, required: true, unique: true },
|
|
9
|
-
userId: { type: String, required: true, index: true },
|
|
10
|
-
token: { type: String, default: null },
|
|
11
|
-
createdAt: { type: Date, required: true },
|
|
12
|
-
lastActiveAt: { type: Date, required: true },
|
|
13
|
-
expiresAt: { type: Date, required: true },
|
|
14
|
-
ipAddress: { type: String },
|
|
15
|
-
userAgent: { type: String },
|
|
16
|
-
}, { collection: "sessions", timestamps: false });
|
|
17
6
|
function getSessionModel() {
|
|
18
7
|
if (appConnection.models["Session"])
|
|
19
8
|
return appConnection.models["Session"];
|
|
9
|
+
const { Schema } = mongoose;
|
|
10
|
+
const sessionSchema = new Schema({
|
|
11
|
+
sessionId: { type: String, required: true, unique: true },
|
|
12
|
+
userId: { type: String, required: true, index: true },
|
|
13
|
+
token: { type: String, default: null },
|
|
14
|
+
createdAt: { type: Date, required: true },
|
|
15
|
+
lastActiveAt: { type: Date, required: true },
|
|
16
|
+
expiresAt: { type: Date, required: true },
|
|
17
|
+
ipAddress: { type: String },
|
|
18
|
+
userAgent: { type: String },
|
|
19
|
+
}, { collection: "sessions", timestamps: false });
|
|
20
20
|
// Add TTL index only when metadata is not persisted — docs auto-delete at expiresAt.
|
|
21
21
|
// When persisting, token is nulled (soft-delete) but the row is kept indefinitely.
|
|
22
22
|
if (!getPersistSessionMetadata()) {
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { getRedis } from "../lib/redis";
|
|
2
2
|
import { getAppName } from "../lib/appConfig";
|
|
3
|
-
import { appConnection } from "../lib/mongo";
|
|
4
|
-
import { Schema } from "mongoose";
|
|
3
|
+
import { appConnection, mongoose } from "../lib/mongo";
|
|
5
4
|
import { isSqliteReady, sqliteGetCache, sqliteSetCache, sqliteDelCache, sqliteDelCachePattern } from "../adapters/sqliteAuth";
|
|
6
5
|
import { memoryGetCache, memorySetCache, memoryDelCache, memoryDelCachePattern } from "../adapters/memoryAuth";
|
|
7
|
-
const cacheSchema = new Schema({
|
|
8
|
-
key: { type: String, required: true, unique: true },
|
|
9
|
-
value: { type: String, required: true },
|
|
10
|
-
expiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
|
|
11
|
-
}, { collection: "cache_entries" });
|
|
12
6
|
function getCacheModel() {
|
|
13
|
-
|
|
14
|
-
appConnection.
|
|
7
|
+
if (appConnection.models["CacheEntry"])
|
|
8
|
+
return appConnection.models["CacheEntry"];
|
|
9
|
+
const { Schema } = mongoose;
|
|
10
|
+
const cacheSchema = new Schema({
|
|
11
|
+
key: { type: String, required: true, unique: true },
|
|
12
|
+
value: { type: String, required: true },
|
|
13
|
+
expiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
|
|
14
|
+
}, { collection: "cache_entries" });
|
|
15
|
+
return appConnection.model("CacheEntry", cacheSchema);
|
|
15
16
|
}
|
|
16
17
|
function isMongoReady() {
|
|
17
18
|
return appConnection.readyState === 1;
|
|
@@ -1,112 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import type { Document, Model } from "mongoose";
|
|
2
|
+
interface IAuthUser {
|
|
3
|
+
email?: string | null;
|
|
4
|
+
password?: string | null;
|
|
5
|
+
/** Compound provider keys: ["google:123456", "apple:000111"] */
|
|
3
6
|
providerIds: string[];
|
|
4
|
-
|
|
7
|
+
/** App-defined roles assigned to this user: ["admin", "editor", ...] */
|
|
5
8
|
roles: string[];
|
|
6
|
-
email
|
|
7
|
-
password?: string | null | undefined;
|
|
8
|
-
} & mongoose.DefaultTimestampProps, {}, {}, {
|
|
9
|
-
id: string;
|
|
10
|
-
}, mongoose.Document<unknown, {}, {
|
|
11
|
-
providerIds: string[];
|
|
9
|
+
/** Whether the user's email address has been verified. */
|
|
12
10
|
emailVerified: boolean;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}, {
|
|
19
|
-
timestamps: true;
|
|
20
|
-
}> & Omit<{
|
|
21
|
-
providerIds: string[];
|
|
22
|
-
emailVerified: boolean;
|
|
23
|
-
roles: string[];
|
|
24
|
-
email?: string | null | undefined;
|
|
25
|
-
password?: string | null | undefined;
|
|
26
|
-
} & mongoose.DefaultTimestampProps & {
|
|
27
|
-
_id: mongoose.Types.ObjectId;
|
|
28
|
-
} & {
|
|
11
|
+
}
|
|
12
|
+
type AuthUserDocument = IAuthUser & Document;
|
|
13
|
+
export declare const AuthUser: Model<AuthUserDocument, {}, {}, {}, Document<unknown, {}, AuthUserDocument, {}, import("mongoose").DefaultSchemaOptions> & IAuthUser & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
|
14
|
+
_id: import("mongoose").Types.ObjectId;
|
|
15
|
+
}> & {
|
|
29
16
|
__v: number;
|
|
30
|
-
}, "id"> & {
|
|
31
|
-
id: string;
|
|
32
|
-
}, mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
|
|
33
|
-
timestamps: true;
|
|
34
|
-
}, {
|
|
35
|
-
providerIds: string[];
|
|
36
|
-
emailVerified: boolean;
|
|
37
|
-
roles: string[];
|
|
38
|
-
email?: string | null | undefined;
|
|
39
|
-
password?: string | null | undefined;
|
|
40
|
-
} & mongoose.DefaultTimestampProps, mongoose.Document<unknown, {}, {
|
|
41
|
-
providerIds: string[];
|
|
42
|
-
emailVerified: boolean;
|
|
43
|
-
roles: string[];
|
|
44
|
-
email?: string | null | undefined;
|
|
45
|
-
password?: string | null | undefined;
|
|
46
|
-
} & mongoose.DefaultTimestampProps, {
|
|
47
|
-
id: string;
|
|
48
|
-
}, mongoose.MergeType<mongoose.DefaultSchemaOptions, {
|
|
49
|
-
timestamps: true;
|
|
50
|
-
}>> & Omit<{
|
|
51
|
-
providerIds: string[];
|
|
52
|
-
emailVerified: boolean;
|
|
53
|
-
roles: string[];
|
|
54
|
-
email?: string | null | undefined;
|
|
55
|
-
password?: string | null | undefined;
|
|
56
|
-
} & mongoose.DefaultTimestampProps & {
|
|
57
|
-
_id: mongoose.Types.ObjectId;
|
|
58
17
|
} & {
|
|
59
|
-
__v: number;
|
|
60
|
-
}, "id"> & {
|
|
61
18
|
id: string;
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
} | {
|
|
65
|
-
[x: string]: mongoose.SchemaDefinitionProperty<any, any, mongoose.Document<unknown, {}, {
|
|
66
|
-
providerIds: string[];
|
|
67
|
-
emailVerified: boolean;
|
|
68
|
-
roles: string[];
|
|
69
|
-
email?: string | null | undefined;
|
|
70
|
-
password?: string | null | undefined;
|
|
71
|
-
} & mongoose.DefaultTimestampProps, {
|
|
72
|
-
id: string;
|
|
73
|
-
}, mongoose.MergeType<mongoose.DefaultSchemaOptions, {
|
|
74
|
-
timestamps: true;
|
|
75
|
-
}>> & Omit<{
|
|
76
|
-
providerIds: string[];
|
|
77
|
-
emailVerified: boolean;
|
|
78
|
-
roles: string[];
|
|
79
|
-
email?: string | null | undefined;
|
|
80
|
-
password?: string | null | undefined;
|
|
81
|
-
} & mongoose.DefaultTimestampProps & {
|
|
82
|
-
_id: mongoose.Types.ObjectId;
|
|
83
|
-
} & {
|
|
84
|
-
__v: number;
|
|
85
|
-
}, "id"> & {
|
|
86
|
-
id: string;
|
|
87
|
-
}> | undefined;
|
|
88
|
-
}, {
|
|
89
|
-
providerIds: string[];
|
|
90
|
-
emailVerified: boolean;
|
|
91
|
-
roles: string[];
|
|
92
|
-
email?: string | null | undefined;
|
|
93
|
-
password?: string | null | undefined;
|
|
94
|
-
createdAt: NativeDate;
|
|
95
|
-
updatedAt: NativeDate;
|
|
96
|
-
} & {
|
|
97
|
-
_id: mongoose.Types.ObjectId;
|
|
98
|
-
} & {
|
|
99
|
-
__v: number;
|
|
100
|
-
}>, {
|
|
101
|
-
providerIds: string[];
|
|
102
|
-
emailVerified: boolean;
|
|
103
|
-
roles: string[];
|
|
104
|
-
email?: string | null | undefined;
|
|
105
|
-
password?: string | null | undefined;
|
|
106
|
-
createdAt: NativeDate;
|
|
107
|
-
updatedAt: NativeDate;
|
|
108
|
-
} & {
|
|
109
|
-
_id: mongoose.Types.ObjectId;
|
|
110
|
-
} & {
|
|
111
|
-
__v: number;
|
|
112
|
-
}>;
|
|
19
|
+
}, any, AuthUserDocument>;
|
|
20
|
+
export {};
|
package/dist/models/AuthUser.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import mongoose from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { authConnection, mongoose } from "../lib/mongo";
|
|
2
|
+
// Lazily register the model — authConnection and mongoose are proxies that
|
|
3
|
+
// resolve once connectAuthMongo() / connectMongo() has been called.
|
|
4
|
+
let _AuthUser = null;
|
|
5
|
+
function getAuthUser() {
|
|
6
|
+
if (!_AuthUser) {
|
|
7
|
+
const { Schema } = mongoose;
|
|
8
|
+
const schema = new Schema({
|
|
9
|
+
email: { type: String, unique: true, sparse: true, lowercase: true },
|
|
10
|
+
password: { type: String },
|
|
11
|
+
/** Compound provider keys: ["google:123456", "apple:000111"] */
|
|
12
|
+
providerIds: [{ type: String }],
|
|
13
|
+
/** App-defined roles assigned to this user: ["admin", "editor", ...] */
|
|
14
|
+
roles: [{ type: String }],
|
|
15
|
+
/** Whether the user's email address has been verified. */
|
|
16
|
+
emailVerified: { type: Boolean, default: false },
|
|
17
|
+
}, { timestamps: true });
|
|
18
|
+
schema.index({ providerIds: 1 });
|
|
19
|
+
_AuthUser = authConnection.model("AuthUser", schema);
|
|
20
|
+
}
|
|
21
|
+
return _AuthUser;
|
|
22
|
+
}
|
|
23
|
+
// Export a Proxy so callers can use AuthUser.findOne() etc. at any time after
|
|
24
|
+
// connectAuthMongo() / connectMongo() has been called.
|
|
25
|
+
export const AuthUser = new Proxy({}, {
|
|
26
|
+
get(_, prop) {
|
|
27
|
+
const model = getAuthUser();
|
|
28
|
+
const val = model[prop];
|
|
29
|
+
return typeof val === "function" ? val.bind(model) : val;
|
|
30
|
+
},
|
|
31
|
+
});
|
package/dist/routes/auth.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { PrimaryField, EmailVerificationConfig } from "../lib/appConfig";
|
|
1
|
+
import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "../lib/appConfig";
|
|
2
2
|
import type { AuthRateLimitConfig } from "../app";
|
|
3
3
|
export interface AuthRouterOptions {
|
|
4
4
|
primaryField: PrimaryField;
|
|
5
5
|
emailVerification?: EmailVerificationConfig;
|
|
6
|
+
passwordReset?: PasswordResetConfig;
|
|
6
7
|
rateLimit?: AuthRateLimitConfig;
|
|
7
8
|
}
|
|
8
|
-
export declare const createAuthRouter: ({ primaryField, emailVerification, rateLimit }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
|
|
9
|
+
export declare const createAuthRouter: ({ primaryField, emailVerification, passwordReset, rateLimit }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
|