@lastshotlabs/bunshot 0.0.8 → 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 +97 -7
- package/dist/adapters/memoryAuth.d.ts +13 -3
- package/dist/adapters/memoryAuth.js +116 -8
- package/dist/adapters/sqliteAuth.d.ts +13 -3
- package/dist/adapters/sqliteAuth.js +93 -15
- package/dist/app.d.ts +39 -2
- package/dist/app.js +23 -5
- package/dist/cli.js +0 -0
- 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 +6 -8
- package/dist/index.js +5 -6
- package/dist/lib/appConfig.d.ts +17 -0
- package/dist/lib/appConfig.js +20 -0
- package/dist/lib/context.d.ts +1 -0
- package/dist/lib/emailVerification.js +11 -10
- package/dist/lib/jwt.d.ts +1 -1
- package/dist/lib/jwt.js +1 -1
- 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.d.ts +20 -3
- package/dist/lib/session.js +288 -35
- package/dist/middleware/cacheResponse.js +10 -9
- package/dist/middleware/identify.js +21 -7
- 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 +139 -4
- package/dist/routes/oauth.js +13 -4
- package/dist/services/auth.d.ts +3 -2
- package/dist/services/auth.js +20 -11
- package/dist/ws/index.js +6 -3
- package/package.json +39 -9
|
@@ -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.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
export interface SessionMetadata {
|
|
2
|
+
ipAddress?: string;
|
|
3
|
+
userAgent?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SessionInfo {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
lastActiveAt: number;
|
|
9
|
+
expiresAt: number;
|
|
10
|
+
ipAddress?: string;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
}
|
|
1
14
|
type SessionStore = "redis" | "mongo" | "sqlite" | "memory";
|
|
2
15
|
export declare const setSessionStore: (store: SessionStore) => void;
|
|
3
|
-
export declare const createSession: (userId: string, token: string) => Promise<void>;
|
|
4
|
-
export declare const getSession: (
|
|
5
|
-
export declare const deleteSession: (
|
|
16
|
+
export declare const createSession: (userId: string, token: string, sessionId: string, metadata?: SessionMetadata) => Promise<void>;
|
|
17
|
+
export declare const getSession: (sessionId: string) => Promise<string | null>;
|
|
18
|
+
export declare const deleteSession: (sessionId: string) => Promise<void>;
|
|
19
|
+
export declare const getUserSessions: (userId: string) => Promise<SessionInfo[]>;
|
|
20
|
+
export declare const getActiveSessionCount: (userId: string) => Promise<number>;
|
|
21
|
+
export declare const evictOldestSession: (userId: string) => Promise<void>;
|
|
22
|
+
export declare const updateSessionLastActive: (sessionId: string) => Promise<void>;
|
|
6
23
|
export {};
|
package/dist/lib/session.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { getRedis } from "./redis";
|
|
2
|
-
import { appConnection } from "./mongo";
|
|
3
|
-
import { getAppName } from "./appConfig";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { memoryCreateSession, memoryGetSession, memoryDeleteSession } from "../adapters/memoryAuth";
|
|
7
|
-
const sessionSchema = new Schema({
|
|
8
|
-
userId: { type: String, required: true, unique: true },
|
|
9
|
-
token: { type: String, required: true },
|
|
10
|
-
expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
|
|
11
|
-
}, { collection: "sessions" });
|
|
2
|
+
import { appConnection, mongoose } from "./mongo";
|
|
3
|
+
import { getAppName, getPersistSessionMetadata, getIncludeInactiveSessions } from "./appConfig";
|
|
4
|
+
import { sqliteCreateSession, sqliteGetSession, sqliteDeleteSession, sqliteGetUserSessions, sqliteGetActiveSessionCount, sqliteEvictOldestSession, sqliteUpdateSessionLastActive, } from "../adapters/sqliteAuth";
|
|
5
|
+
import { memoryCreateSession, memoryGetSession, memoryDeleteSession, memoryGetUserSessions, memoryGetActiveSessionCount, memoryEvictOldestSession, memoryUpdateSessionLastActive, } from "../adapters/memoryAuth";
|
|
12
6
|
function getSessionModel() {
|
|
13
|
-
|
|
14
|
-
appConnection.
|
|
7
|
+
if (appConnection.models["Session"])
|
|
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
|
+
// Add TTL index only when metadata is not persisted — docs auto-delete at expiresAt.
|
|
21
|
+
// When persisting, token is nulled (soft-delete) but the row is kept indefinitely.
|
|
22
|
+
if (!getPersistSessionMetadata()) {
|
|
23
|
+
sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
24
|
+
}
|
|
25
|
+
return appConnection.model("Session", sessionSchema);
|
|
15
26
|
}
|
|
16
27
|
let _store = "redis";
|
|
17
28
|
export const setSessionStore = (store) => { _store = store; };
|
|
@@ -19,50 +30,292 @@ export const setSessionStore = (store) => { _store = store; };
|
|
|
19
30
|
// TTL
|
|
20
31
|
// ---------------------------------------------------------------------------
|
|
21
32
|
const TTL_SECONDS = 60 * 60 * 24 * 7; // 7 days
|
|
33
|
+
const TTL_MS = TTL_SECONDS * 1000;
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Redis helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
function redisSessionKey(sessionId) {
|
|
38
|
+
return `session:${getAppName()}:${sessionId}`;
|
|
39
|
+
}
|
|
40
|
+
function redisUserSessionsKey(userId) {
|
|
41
|
+
return `usersessions:${getAppName()}:${userId}`;
|
|
42
|
+
}
|
|
43
|
+
async function redisCreateSession(userId, token, sessionId, metadata) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const expiresAt = now + TTL_MS;
|
|
46
|
+
const record = JSON.stringify({
|
|
47
|
+
sessionId, userId, token,
|
|
48
|
+
createdAt: now, lastActiveAt: now, expiresAt,
|
|
49
|
+
ipAddress: metadata?.ipAddress,
|
|
50
|
+
userAgent: metadata?.userAgent,
|
|
51
|
+
});
|
|
52
|
+
const redis = getRedis();
|
|
53
|
+
const persist = getPersistSessionMetadata();
|
|
54
|
+
if (persist) {
|
|
55
|
+
await redis.set(redisSessionKey(sessionId), record);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
await redis.set(redisSessionKey(sessionId), record, "EX", TTL_SECONDS);
|
|
59
|
+
}
|
|
60
|
+
// Sorted set: score = createdAt (oldest first)
|
|
61
|
+
await redis.zadd(redisUserSessionsKey(userId), now, sessionId);
|
|
62
|
+
}
|
|
63
|
+
async function redisGetSession(sessionId) {
|
|
64
|
+
const raw = await getRedis().get(redisSessionKey(sessionId));
|
|
65
|
+
if (!raw)
|
|
66
|
+
return null;
|
|
67
|
+
const rec = JSON.parse(raw);
|
|
68
|
+
if (!rec.token)
|
|
69
|
+
return null;
|
|
70
|
+
if (rec.expiresAt <= Date.now())
|
|
71
|
+
return null;
|
|
72
|
+
return rec.token;
|
|
73
|
+
}
|
|
74
|
+
async function redisDeleteSession(sessionId) {
|
|
75
|
+
const redis = getRedis();
|
|
76
|
+
const raw = await redis.get(redisSessionKey(sessionId));
|
|
77
|
+
if (!raw)
|
|
78
|
+
return;
|
|
79
|
+
const rec = JSON.parse(raw);
|
|
80
|
+
const persist = getPersistSessionMetadata();
|
|
81
|
+
if (persist) {
|
|
82
|
+
const updated = { ...JSON.parse(raw), token: null };
|
|
83
|
+
await redis.set(redisSessionKey(sessionId), JSON.stringify(updated));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
await redis.del(redisSessionKey(sessionId));
|
|
87
|
+
}
|
|
88
|
+
if (!persist) {
|
|
89
|
+
await redis.zrem(redisUserSessionsKey(rec.userId), sessionId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function redisGetUserSessions(userId) {
|
|
93
|
+
const redis = getRedis();
|
|
94
|
+
const sessionIds = await redis.zrange(redisUserSessionsKey(userId), 0, -1);
|
|
95
|
+
if (!sessionIds.length)
|
|
96
|
+
return [];
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const raws = await redis.mget(...sessionIds.map(redisSessionKey));
|
|
99
|
+
const results = [];
|
|
100
|
+
const toRemove = [];
|
|
101
|
+
for (let i = 0; i < sessionIds.length; i++) {
|
|
102
|
+
const raw = raws[i];
|
|
103
|
+
if (!raw) {
|
|
104
|
+
toRemove.push(sessionIds[i]);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const rec = JSON.parse(raw);
|
|
108
|
+
const isActive = !!rec.token && rec.expiresAt > now;
|
|
109
|
+
if (!isActive && !getPersistSessionMetadata()) {
|
|
110
|
+
toRemove.push(sessionIds[i]);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!isActive && !getIncludeInactiveSessions())
|
|
114
|
+
continue;
|
|
115
|
+
results.push({
|
|
116
|
+
sessionId: rec.sessionId,
|
|
117
|
+
createdAt: Number(rec.createdAt),
|
|
118
|
+
lastActiveAt: Number(rec.lastActiveAt),
|
|
119
|
+
expiresAt: Number(rec.expiresAt),
|
|
120
|
+
ipAddress: rec.ipAddress,
|
|
121
|
+
userAgent: rec.userAgent,
|
|
122
|
+
isActive,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (toRemove.length) {
|
|
126
|
+
await redis.zrem(redisUserSessionsKey(userId), ...toRemove);
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
async function redisGetActiveSessionCount(userId) {
|
|
131
|
+
const sessions = await redisGetUserSessions(userId);
|
|
132
|
+
return sessions.filter((s) => s.isActive).length;
|
|
133
|
+
}
|
|
134
|
+
async function redisEvictOldestSession(userId) {
|
|
135
|
+
const redis = getRedis();
|
|
136
|
+
// Sorted set is ordered oldest-first (score = createdAt)
|
|
137
|
+
const sessionIds = await redis.zrange(redisUserSessionsKey(userId), 0, -1);
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
for (const sessionId of sessionIds) {
|
|
140
|
+
const raw = await redis.get(redisSessionKey(sessionId));
|
|
141
|
+
if (!raw) {
|
|
142
|
+
await redis.zrem(redisUserSessionsKey(userId), sessionId);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const rec = JSON.parse(raw);
|
|
146
|
+
if (rec.token && rec.expiresAt > now) {
|
|
147
|
+
await redisDeleteSession(sessionId);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function redisUpdateSessionLastActive(sessionId) {
|
|
153
|
+
const redis = getRedis();
|
|
154
|
+
const raw = await redis.get(redisSessionKey(sessionId));
|
|
155
|
+
if (!raw)
|
|
156
|
+
return;
|
|
157
|
+
const rec = JSON.parse(raw);
|
|
158
|
+
rec.lastActiveAt = Date.now();
|
|
159
|
+
if (getPersistSessionMetadata()) {
|
|
160
|
+
await redis.set(redisSessionKey(sessionId), JSON.stringify(rec));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
if (rec.expiresAt <= now) {
|
|
165
|
+
await redisDeleteSession(sessionId);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const ttlRemaining = Math.max(1, Math.ceil((rec.expiresAt - now) / 1000));
|
|
169
|
+
await redis.set(redisSessionKey(sessionId), JSON.stringify(rec), "EX", ttlRemaining);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Mongo helpers
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
async function mongoGetUserSessions(userId) {
|
|
176
|
+
const now = new Date();
|
|
177
|
+
const includeInactive = getIncludeInactiveSessions();
|
|
178
|
+
const persist = getPersistSessionMetadata();
|
|
179
|
+
const query = { userId };
|
|
180
|
+
if (!includeInactive) {
|
|
181
|
+
query.token = { $ne: null };
|
|
182
|
+
query.expiresAt = { $gt: now };
|
|
183
|
+
}
|
|
184
|
+
const docs = await getSessionModel().find(query).lean();
|
|
185
|
+
const results = [];
|
|
186
|
+
for (const doc of docs) {
|
|
187
|
+
const isActive = !!doc.token && doc.expiresAt > now;
|
|
188
|
+
if (!isActive && !persist)
|
|
189
|
+
continue;
|
|
190
|
+
if (!isActive && !includeInactive)
|
|
191
|
+
continue;
|
|
192
|
+
results.push({
|
|
193
|
+
sessionId: doc.sessionId,
|
|
194
|
+
createdAt: doc.createdAt.getTime(),
|
|
195
|
+
lastActiveAt: doc.lastActiveAt.getTime(),
|
|
196
|
+
expiresAt: doc.expiresAt.getTime(),
|
|
197
|
+
ipAddress: doc.ipAddress,
|
|
198
|
+
userAgent: doc.userAgent,
|
|
199
|
+
isActive,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
22
204
|
// ---------------------------------------------------------------------------
|
|
23
205
|
// Public API
|
|
24
206
|
// ---------------------------------------------------------------------------
|
|
25
|
-
export const createSession = async (userId, token) => {
|
|
207
|
+
export const createSession = async (userId, token, sessionId, metadata) => {
|
|
26
208
|
if (_store === "memory") {
|
|
27
|
-
memoryCreateSession(userId, token);
|
|
209
|
+
memoryCreateSession(userId, token, sessionId, metadata);
|
|
28
210
|
return;
|
|
29
211
|
}
|
|
30
212
|
if (_store === "sqlite") {
|
|
31
|
-
sqliteCreateSession(userId, token);
|
|
213
|
+
sqliteCreateSession(userId, token, sessionId, metadata);
|
|
32
214
|
return;
|
|
33
215
|
}
|
|
34
|
-
if (_store === "
|
|
35
|
-
|
|
36
|
-
await getSessionModel().updateOne({ userId }, { $set: { token, expiresAt } }, { upsert: true });
|
|
216
|
+
if (_store === "redis") {
|
|
217
|
+
await redisCreateSession(userId, token, sessionId, metadata);
|
|
37
218
|
return;
|
|
38
219
|
}
|
|
39
|
-
|
|
220
|
+
// mongo
|
|
221
|
+
const now = new Date();
|
|
222
|
+
const expiresAt = new Date(Date.now() + TTL_MS);
|
|
223
|
+
await getSessionModel().create({
|
|
224
|
+
sessionId, userId, token,
|
|
225
|
+
createdAt: now, lastActiveAt: now, expiresAt,
|
|
226
|
+
ipAddress: metadata?.ipAddress,
|
|
227
|
+
userAgent: metadata?.userAgent,
|
|
228
|
+
});
|
|
40
229
|
};
|
|
41
|
-
export const getSession = async (
|
|
230
|
+
export const getSession = async (sessionId) => {
|
|
42
231
|
if (_store === "memory")
|
|
43
|
-
return memoryGetSession(
|
|
232
|
+
return memoryGetSession(sessionId);
|
|
44
233
|
if (_store === "sqlite")
|
|
45
|
-
return sqliteGetSession(
|
|
46
|
-
if (_store === "
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
234
|
+
return sqliteGetSession(sessionId);
|
|
235
|
+
if (_store === "redis")
|
|
236
|
+
return redisGetSession(sessionId);
|
|
237
|
+
// mongo
|
|
238
|
+
const doc = await getSessionModel()
|
|
239
|
+
.findOne({ sessionId, expiresAt: { $gt: new Date() } }, "token")
|
|
240
|
+
.lean();
|
|
241
|
+
return doc?.token ?? null;
|
|
242
|
+
};
|
|
243
|
+
export const deleteSession = async (sessionId) => {
|
|
244
|
+
if (_store === "memory") {
|
|
245
|
+
memoryDeleteSession(sessionId);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (_store === "sqlite") {
|
|
249
|
+
sqliteDeleteSession(sessionId);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (_store === "redis") {
|
|
253
|
+
await redisDeleteSession(sessionId);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// mongo
|
|
257
|
+
if (getPersistSessionMetadata()) {
|
|
258
|
+
await getSessionModel().updateOne({ sessionId }, { $set: { token: null } });
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
await getSessionModel().deleteOne({ sessionId });
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
export const getUserSessions = async (userId) => {
|
|
265
|
+
if (_store === "memory")
|
|
266
|
+
return memoryGetUserSessions(userId);
|
|
267
|
+
if (_store === "sqlite")
|
|
268
|
+
return sqliteGetUserSessions(userId);
|
|
269
|
+
if (_store === "redis")
|
|
270
|
+
return redisGetUserSessions(userId);
|
|
271
|
+
return mongoGetUserSessions(userId);
|
|
272
|
+
};
|
|
273
|
+
export const getActiveSessionCount = async (userId) => {
|
|
274
|
+
if (_store === "memory")
|
|
275
|
+
return memoryGetActiveSessionCount(userId);
|
|
276
|
+
if (_store === "sqlite")
|
|
277
|
+
return sqliteGetActiveSessionCount(userId);
|
|
278
|
+
if (_store === "redis")
|
|
279
|
+
return redisGetActiveSessionCount(userId);
|
|
280
|
+
// mongo
|
|
281
|
+
const now = new Date();
|
|
282
|
+
return getSessionModel().countDocuments({ userId, token: { $ne: null }, expiresAt: { $gt: now } });
|
|
283
|
+
};
|
|
284
|
+
export const evictOldestSession = async (userId) => {
|
|
285
|
+
if (_store === "memory") {
|
|
286
|
+
memoryEvictOldestSession(userId);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (_store === "sqlite") {
|
|
290
|
+
sqliteEvictOldestSession(userId);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (_store === "redis") {
|
|
294
|
+
await redisEvictOldestSession(userId);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// mongo — oldest active session by createdAt
|
|
298
|
+
const now = new Date();
|
|
299
|
+
const oldest = await getSessionModel()
|
|
300
|
+
.findOne({ userId, token: { $ne: null }, expiresAt: { $gt: now } }, "sessionId")
|
|
301
|
+
.sort({ createdAt: 1 })
|
|
302
|
+
.lean();
|
|
303
|
+
if (oldest)
|
|
304
|
+
await deleteSession(oldest.sessionId);
|
|
53
305
|
};
|
|
54
|
-
export const
|
|
306
|
+
export const updateSessionLastActive = async (sessionId) => {
|
|
55
307
|
if (_store === "memory") {
|
|
56
|
-
|
|
308
|
+
memoryUpdateSessionLastActive(sessionId);
|
|
57
309
|
return;
|
|
58
310
|
}
|
|
59
311
|
if (_store === "sqlite") {
|
|
60
|
-
|
|
312
|
+
sqliteUpdateSessionLastActive(sessionId);
|
|
61
313
|
return;
|
|
62
314
|
}
|
|
63
|
-
if (_store === "
|
|
64
|
-
await
|
|
315
|
+
if (_store === "redis") {
|
|
316
|
+
await redisUpdateSessionLastActive(sessionId);
|
|
65
317
|
return;
|
|
66
318
|
}
|
|
67
|
-
|
|
319
|
+
// mongo
|
|
320
|
+
await getSessionModel().updateOne({ sessionId }, { $set: { lastActiveAt: new Date() } });
|
|
68
321
|
};
|
|
@@ -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,25 +1,39 @@
|
|
|
1
1
|
import { getCookie } from "hono/cookie";
|
|
2
2
|
import { verifyToken } from "../lib/jwt";
|
|
3
|
-
import { getSession } from "../lib/session";
|
|
3
|
+
import { getSession, updateSessionLastActive } from "../lib/session";
|
|
4
4
|
import { COOKIE_TOKEN, HEADER_USER_TOKEN } from "../lib/constants";
|
|
5
5
|
import { log } from "../lib/logger";
|
|
6
|
+
import { getTrackLastActive } from "../lib/appConfig";
|
|
6
7
|
export const identify = async (c, next) => {
|
|
7
8
|
c.set("authUserId", null);
|
|
8
9
|
c.set("roles", null);
|
|
10
|
+
c.set("sessionId", null);
|
|
9
11
|
// cookie for browsers, x-user-token header for non-browser clients
|
|
10
12
|
const token = getCookie(c, COOKIE_TOKEN) ?? c.req.header(HEADER_USER_TOKEN) ?? null;
|
|
11
13
|
log(`[identify] token=${token ? "present" : "absent"}`);
|
|
12
14
|
if (token) {
|
|
13
15
|
try {
|
|
14
16
|
const payload = await verifyToken(token);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
c.set("authUserId", payload.sub);
|
|
19
|
-
log(`[identify] authUserId=${payload.sub}`);
|
|
17
|
+
const sessionId = payload.sid;
|
|
18
|
+
if (!sessionId) {
|
|
19
|
+
log("[identify] token missing sid claim — unauthenticated");
|
|
20
20
|
}
|
|
21
21
|
else {
|
|
22
|
-
|
|
22
|
+
const stored = await getSession(sessionId);
|
|
23
|
+
log(`[identify] token for authUserId=${payload.sub} verified, checking session...`);
|
|
24
|
+
if (stored === token) {
|
|
25
|
+
c.set("authUserId", payload.sub);
|
|
26
|
+
c.set("sessionId", sessionId);
|
|
27
|
+
log(`[identify] authUserId=${payload.sub} sessionId=${sessionId}`);
|
|
28
|
+
if (getTrackLastActive()) {
|
|
29
|
+
updateSessionLastActive(sessionId).catch(() => {
|
|
30
|
+
log(`[identify] failed to update lastActiveAt for sessionId=${sessionId}`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
log("[identify] token/session mismatch — unauthenticated");
|
|
36
|
+
}
|
|
23
37
|
}
|
|
24
38
|
}
|
|
25
39
|
catch {
|
|
@@ -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 {};
|