@mostajs/auth-lite 0.1.1 → 0.2.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/dist/index.d.ts +0 -21
- package/dist/index.js +17 -91
- package/dist/next.d.ts +27 -0
- package/dist/next.js +112 -0
- package/package.json +6 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @mostajs/auth-lite — minimal, dependency-free email/password + session auth
|
|
3
|
-
* for Next.js (App Router) on top of @mostajs/orm.
|
|
4
|
-
*
|
|
5
|
-
* Why "lite": it boots in WebContainers (Bolt.new / StackBlitz) and the edge —
|
|
6
|
-
* no native addon (no bcrypt/argon2), and it sets the session cookie on the
|
|
7
|
-
* NextResponse object inside Route Handlers, which sidesteps the AsyncLocalStorage
|
|
8
|
-
* pitfalls of `cookies()` after a DB call in constrained runtimes.
|
|
9
|
-
*
|
|
10
|
-
* Password hashing is salted, iterated SHA-256 (works everywhere). For a
|
|
11
|
-
* production server you can swap in argon2/scrypt — the API is unchanged.
|
|
12
|
-
*
|
|
13
|
-
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
14
|
-
*/
|
|
15
|
-
import type { NextRequest } from 'next/server';
|
|
16
1
|
import type { EntitySchema } from '@mostajs/orm';
|
|
17
2
|
export declare function hashPassword(password: string): string;
|
|
18
3
|
export declare function verifyPassword(password: string, stored: string): boolean;
|
|
@@ -43,9 +28,3 @@ export interface AuthLiteConfig {
|
|
|
43
28
|
/** Redirect on signup error (default "/signup?error=<kind>"). */
|
|
44
29
|
signupErrorPath?: (kind: 'invalid' | 'exists') => string;
|
|
45
30
|
}
|
|
46
|
-
export declare function createAuthHandlers(config: AuthLiteConfig): {
|
|
47
|
-
login: (req: NextRequest) => Promise<import("next/server").NextResponse<unknown>>;
|
|
48
|
-
signup: (req: NextRequest) => Promise<import("next/server").NextResponse<unknown>>;
|
|
49
|
-
logout: (req: NextRequest) => Promise<import("next/server").NextResponse<unknown>>;
|
|
50
|
-
};
|
|
51
|
-
export declare function createGetCurrentUser<TUser = unknown>(config: AuthLiteConfig): () => Promise<TUser | null>;
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from 'crypto';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* @mostajs/auth-lite — minimal email/password + session auth on @mostajs/orm.
|
|
3
|
+
*
|
|
4
|
+
* This entry (`@mostajs/auth-lite`) is the **framework-agnostic core** :
|
|
5
|
+
* password hashing + the `Session` schema + the shared types. It imports
|
|
6
|
+
* **no `next`**, so it loads in Node / edge / WebContainer and is unit-testable.
|
|
7
|
+
*
|
|
8
|
+
* The **Next.js adapter** (Route Handlers + `getCurrentUser`) lives in the
|
|
9
|
+
* `@mostajs/auth-lite/next` subpath — it statically imports `next/server` and
|
|
10
|
+
* `next/headers` so that `cookies()` is the first `await` (request scope intact
|
|
11
|
+
* in WebContainers).
|
|
12
|
+
*
|
|
13
|
+
* Password hashing is salted, iterated SHA-256 (no native addon — boots
|
|
14
|
+
* everywhere). For a production server you can swap in argon2/scrypt; the API
|
|
15
|
+
* is unchanged.
|
|
16
|
+
*
|
|
17
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
8
18
|
*/
|
|
9
|
-
|
|
10
|
-
const { NextResponse } = await import('next/server');
|
|
11
|
-
return new NextResponse(null, { status: 303, headers: { Location: location } });
|
|
12
|
-
}
|
|
19
|
+
import { createHash, randomBytes, timingSafeEqual } from 'crypto';
|
|
13
20
|
// ---------------------------------------------------------------------------
|
|
14
21
|
// Password hashing — salted, iterated SHA-256 (no native addon, boots anywhere)
|
|
15
22
|
// ---------------------------------------------------------------------------
|
|
@@ -49,84 +56,3 @@ export const SessionSchema = {
|
|
|
49
56
|
indexes: [{ fields: ['token'], unique: true }],
|
|
50
57
|
timestamps: true,
|
|
51
58
|
};
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Route Handlers — login / signup / logout (set the cookie on the response)
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
export function createAuthHandlers(config) {
|
|
56
|
-
const cookie = config.cookieName ?? 'session';
|
|
57
|
-
const ttlMs = (config.ttlDays ?? 7) * 86400000;
|
|
58
|
-
const afterAuth = config.afterAuth ?? '/dashboard';
|
|
59
|
-
const afterLogout = config.afterLogout ?? '/';
|
|
60
|
-
const loginError = config.loginErrorPath ?? '/login?error=invalid';
|
|
61
|
-
const signupError = config.signupErrorPath ?? ((k) => `/signup?error=${k}`);
|
|
62
|
-
async function startSession(sessions, userId) {
|
|
63
|
-
const token = randomBytes(32).toString('hex');
|
|
64
|
-
const expiresAt = new Date(Date.now() + ttlMs);
|
|
65
|
-
await sessions.create({ token, user: userId, expiresAt });
|
|
66
|
-
const res = await see(afterAuth);
|
|
67
|
-
res.cookies.set(cookie, token, { httpOnly: true, sameSite: 'lax', path: '/', expires: expiresAt });
|
|
68
|
-
return res;
|
|
69
|
-
}
|
|
70
|
-
/** POST handler — verify credentials, start a session. */
|
|
71
|
-
async function login(req) {
|
|
72
|
-
const form = await req.formData();
|
|
73
|
-
const email = String(form.get('email') ?? '').toLowerCase().trim();
|
|
74
|
-
const password = String(form.get('password') ?? '');
|
|
75
|
-
const { users, sessions } = await config.getRepos();
|
|
76
|
-
const user = await users.findOne({ email });
|
|
77
|
-
if (!user || !verifyPassword(password, user.passwordHash)) {
|
|
78
|
-
return see(loginError);
|
|
79
|
-
}
|
|
80
|
-
return startSession(sessions, user.id);
|
|
81
|
-
}
|
|
82
|
-
/** POST handler — create the account, start a session. */
|
|
83
|
-
async function signup(req) {
|
|
84
|
-
const form = await req.formData();
|
|
85
|
-
const email = String(form.get('email') ?? '').toLowerCase().trim();
|
|
86
|
-
const name = String(form.get('name') ?? '').trim();
|
|
87
|
-
const password = String(form.get('password') ?? '');
|
|
88
|
-
if (!email || !name || password.length < 6) {
|
|
89
|
-
return see(signupError('invalid'));
|
|
90
|
-
}
|
|
91
|
-
const { users, sessions } = await config.getRepos();
|
|
92
|
-
if (await users.findOne({ email })) {
|
|
93
|
-
return see(signupError('exists'));
|
|
94
|
-
}
|
|
95
|
-
const user = await users.create({ email, name, passwordHash: hashPassword(password) });
|
|
96
|
-
return startSession(sessions, user.id);
|
|
97
|
-
}
|
|
98
|
-
/** POST handler — destroy the session (DB + cookie). */
|
|
99
|
-
async function logout(req) {
|
|
100
|
-
const token = req.cookies.get(cookie)?.value;
|
|
101
|
-
if (token) {
|
|
102
|
-
const { sessions } = await config.getRepos();
|
|
103
|
-
const session = await sessions.findOne({ token });
|
|
104
|
-
if (session)
|
|
105
|
-
await sessions.delete(session.id);
|
|
106
|
-
}
|
|
107
|
-
const res = await see(afterLogout);
|
|
108
|
-
res.cookies.delete(cookie);
|
|
109
|
-
return res;
|
|
110
|
-
}
|
|
111
|
-
return { login, signup, logout };
|
|
112
|
-
}
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
// getCurrentUser — read the session in Server Components (cookie read before DB)
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
export function createGetCurrentUser(config) {
|
|
117
|
-
const cookie = config.cookieName ?? 'session';
|
|
118
|
-
return async function getCurrentUser() {
|
|
119
|
-
const { cookies } = await import('next/headers');
|
|
120
|
-
const token = (await cookies()).get(cookie)?.value;
|
|
121
|
-
if (!token)
|
|
122
|
-
return null;
|
|
123
|
-
const { sessions } = await config.getRepos();
|
|
124
|
-
const session = await sessions.findOne({ token });
|
|
125
|
-
if (!session)
|
|
126
|
-
return null;
|
|
127
|
-
if (new Date(session.expiresAt) < new Date())
|
|
128
|
-
return null;
|
|
129
|
-
const populated = (await sessions.findByIdWithRelations(session.id, ['user']));
|
|
130
|
-
return populated?.user ?? null;
|
|
131
|
-
};
|
|
132
|
-
}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/auth-lite/next — Next.js (App Router) adapter.
|
|
3
|
+
*
|
|
4
|
+
* Route Handlers (login/signup/logout) + `getCurrentUser`. Imports `next/server`
|
|
5
|
+
* and `next/headers` **statically** on purpose:
|
|
6
|
+
* - the cookie is set on the `NextResponse` object (no `cookies()` write),
|
|
7
|
+
* - redirects use a **relative** `Location` → resolved by the browser against
|
|
8
|
+
* the public request URL, so it works in WebContainers (StackBlitz/Bolt,
|
|
9
|
+
* which don't forward the public host) and behind a reverse proxy,
|
|
10
|
+
* - `getCurrentUser` calls `cookies()` as the **first `await`** (no dynamic
|
|
11
|
+
* `import` before it) → the request AsyncLocalStorage scope stays intact in
|
|
12
|
+
* constrained runtimes (a preceding `await import(...)` loses it).
|
|
13
|
+
*
|
|
14
|
+
* The framework-agnostic core (hashing, `SessionSchema`, types) is in the root
|
|
15
|
+
* entry `@mostajs/auth-lite`.
|
|
16
|
+
*
|
|
17
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
18
|
+
*/
|
|
19
|
+
import { type NextRequest, NextResponse } from 'next/server';
|
|
20
|
+
import { type AuthRepo, type AuthLiteConfig } from './index.js';
|
|
21
|
+
export type { AuthRepo, AuthLiteConfig };
|
|
22
|
+
export declare function createAuthHandlers(config: AuthLiteConfig): {
|
|
23
|
+
login: (req: NextRequest) => Promise<NextResponse<unknown>>;
|
|
24
|
+
signup: (req: NextRequest) => Promise<NextResponse<unknown>>;
|
|
25
|
+
logout: (req: NextRequest) => Promise<NextResponse<unknown>>;
|
|
26
|
+
};
|
|
27
|
+
export declare function createGetCurrentUser<TUser = unknown>(config: AuthLiteConfig): () => Promise<TUser | null>;
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/auth-lite/next — Next.js (App Router) adapter.
|
|
3
|
+
*
|
|
4
|
+
* Route Handlers (login/signup/logout) + `getCurrentUser`. Imports `next/server`
|
|
5
|
+
* and `next/headers` **statically** on purpose:
|
|
6
|
+
* - the cookie is set on the `NextResponse` object (no `cookies()` write),
|
|
7
|
+
* - redirects use a **relative** `Location` → resolved by the browser against
|
|
8
|
+
* the public request URL, so it works in WebContainers (StackBlitz/Bolt,
|
|
9
|
+
* which don't forward the public host) and behind a reverse proxy,
|
|
10
|
+
* - `getCurrentUser` calls `cookies()` as the **first `await`** (no dynamic
|
|
11
|
+
* `import` before it) → the request AsyncLocalStorage scope stays intact in
|
|
12
|
+
* constrained runtimes (a preceding `await import(...)` loses it).
|
|
13
|
+
*
|
|
14
|
+
* The framework-agnostic core (hashing, `SessionSchema`, types) is in the root
|
|
15
|
+
* entry `@mostajs/auth-lite`.
|
|
16
|
+
*
|
|
17
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
18
|
+
*/
|
|
19
|
+
import { NextResponse } from 'next/server';
|
|
20
|
+
import { cookies } from 'next/headers';
|
|
21
|
+
import { randomBytes } from 'crypto';
|
|
22
|
+
import { hashPassword, verifyPassword } from './index.js';
|
|
23
|
+
/**
|
|
24
|
+
* 303 response with a **relative** `Location` (e.g. `/dashboard`). The browser
|
|
25
|
+
* resolves it against the current request's public URL → works in WebContainer
|
|
26
|
+
* / reverse-proxy / localhost without the server knowing its own host.
|
|
27
|
+
*/
|
|
28
|
+
function see(location) {
|
|
29
|
+
return new NextResponse(null, { status: 303, headers: { Location: location } });
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Route Handlers — login / signup / logout (cookie set on the response)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
export function createAuthHandlers(config) {
|
|
35
|
+
const cookie = config.cookieName ?? 'session';
|
|
36
|
+
const ttlMs = (config.ttlDays ?? 7) * 86400000;
|
|
37
|
+
const afterAuth = config.afterAuth ?? '/dashboard';
|
|
38
|
+
const afterLogout = config.afterLogout ?? '/';
|
|
39
|
+
const loginError = config.loginErrorPath ?? '/login?error=invalid';
|
|
40
|
+
const signupError = config.signupErrorPath ?? ((k) => `/signup?error=${k}`);
|
|
41
|
+
async function openSession(sessions, userId) {
|
|
42
|
+
const token = randomBytes(32).toString('hex');
|
|
43
|
+
const expiresAt = new Date(Date.now() + ttlMs);
|
|
44
|
+
await sessions.create({ token, user: userId, expiresAt });
|
|
45
|
+
const res = see(afterAuth);
|
|
46
|
+
res.cookies.set(cookie, token, { httpOnly: true, sameSite: 'lax', path: '/', expires: expiresAt });
|
|
47
|
+
return res;
|
|
48
|
+
}
|
|
49
|
+
/** POST handler — verify credentials, start a session. */
|
|
50
|
+
async function login(req) {
|
|
51
|
+
const form = await req.formData();
|
|
52
|
+
const email = String(form.get('email') ?? '').toLowerCase().trim();
|
|
53
|
+
const password = String(form.get('password') ?? '');
|
|
54
|
+
const { users, sessions } = await config.getRepos();
|
|
55
|
+
const user = await users.findOne({ email });
|
|
56
|
+
if (!user || !verifyPassword(password, user.passwordHash)) {
|
|
57
|
+
return see(loginError);
|
|
58
|
+
}
|
|
59
|
+
return openSession(sessions, user.id);
|
|
60
|
+
}
|
|
61
|
+
/** POST handler — create the account, start a session. */
|
|
62
|
+
async function signup(req) {
|
|
63
|
+
const form = await req.formData();
|
|
64
|
+
const email = String(form.get('email') ?? '').toLowerCase().trim();
|
|
65
|
+
const name = String(form.get('name') ?? '').trim();
|
|
66
|
+
const password = String(form.get('password') ?? '');
|
|
67
|
+
if (!email || !name || password.length < 6) {
|
|
68
|
+
return see(signupError('invalid'));
|
|
69
|
+
}
|
|
70
|
+
const { users, sessions } = await config.getRepos();
|
|
71
|
+
if (await users.findOne({ email })) {
|
|
72
|
+
return see(signupError('exists'));
|
|
73
|
+
}
|
|
74
|
+
const user = await users.create({ email, name, passwordHash: hashPassword(password) });
|
|
75
|
+
return openSession(sessions, user.id);
|
|
76
|
+
}
|
|
77
|
+
/** POST handler — destroy the session (DB + cookie). */
|
|
78
|
+
async function logout(req) {
|
|
79
|
+
const token = req.cookies.get(cookie)?.value;
|
|
80
|
+
if (token) {
|
|
81
|
+
const { sessions } = await config.getRepos();
|
|
82
|
+
const session = await sessions.findOne({ token });
|
|
83
|
+
if (session)
|
|
84
|
+
await sessions.delete(session.id);
|
|
85
|
+
}
|
|
86
|
+
const res = see(afterLogout);
|
|
87
|
+
res.cookies.delete(cookie);
|
|
88
|
+
return res;
|
|
89
|
+
}
|
|
90
|
+
return { login, signup, logout };
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// getCurrentUser — read the session in Server Components.
|
|
94
|
+
// `cookies()` MUST be the first `await` (no preceding await/import) so the
|
|
95
|
+
// request scope survives in WebContainers.
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
export function createGetCurrentUser(config) {
|
|
98
|
+
const cookie = config.cookieName ?? 'session';
|
|
99
|
+
return async function getCurrentUser() {
|
|
100
|
+
const token = (await cookies()).get(cookie)?.value;
|
|
101
|
+
if (!token)
|
|
102
|
+
return null;
|
|
103
|
+
const { sessions } = await config.getRepos();
|
|
104
|
+
const session = await sessions.findOne({ token });
|
|
105
|
+
if (!session)
|
|
106
|
+
return null;
|
|
107
|
+
if (new Date(session.expiresAt) < new Date())
|
|
108
|
+
return null;
|
|
109
|
+
const populated = (await sessions.findByIdWithRelations(session.id, ['user']));
|
|
110
|
+
return populated?.user ?? null;
|
|
111
|
+
};
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/auth-lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Minimal email/password + session auth for Next.js on @mostajs/orm. No native addon — boots in Bolt.new / StackBlitz / edge.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./next": {
|
|
15
|
+
"types": "./dist/next.d.ts",
|
|
16
|
+
"import": "./dist/next.js",
|
|
17
|
+
"default": "./dist/next.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"files": [
|