@binary1702/auth 0.2.0 → 0.2.1
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/cli.js +15 -15
- package/dist/core/db.d.ts +4 -4
- package/dist/core/db.js +11 -11
- package/dist/core/html.js +2 -2
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +3 -3
- package/dist/core/signup.d.ts +2 -2
- package/dist/core/signup.js +4 -4
- package/dist/next/index.js +13 -13
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -197,22 +197,22 @@ async function stepSchema() {
|
|
|
197
197
|
await ensureSchema().catch((e) => fail(`Could not reach the database: ${e.message}`));
|
|
198
198
|
done("auth_users table ready");
|
|
199
199
|
}
|
|
200
|
-
async function createUserInteractive(
|
|
200
|
+
async function createUserInteractive(presetEmail) {
|
|
201
201
|
const { createUser, findUser } = await db();
|
|
202
202
|
const { hashPassword } = await import("./core/password.js");
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
205
|
-
fail("
|
|
206
|
-
if (await findUser(
|
|
207
|
-
fail(`User "${
|
|
203
|
+
const email = (presetEmail ?? (await ask("Email"))).trim().toLowerCase();
|
|
204
|
+
if (!email)
|
|
205
|
+
fail("Email is required.");
|
|
206
|
+
if (await findUser(email))
|
|
207
|
+
fail(`User "${email}" already exists.`);
|
|
208
208
|
const password = await ask("Password", { hidden: true });
|
|
209
209
|
if (password.length < 8)
|
|
210
210
|
fail("Password must be at least 8 characters.");
|
|
211
211
|
const again = await ask("Password again", { hidden: true });
|
|
212
212
|
if (password !== again)
|
|
213
213
|
fail("Passwords do not match.");
|
|
214
|
-
await createUser(
|
|
215
|
-
done(`user "${
|
|
214
|
+
await createUser(email, await hashPassword(password));
|
|
215
|
+
done(`user "${email}" created`);
|
|
216
216
|
}
|
|
217
217
|
async function stepFirstUser() {
|
|
218
218
|
const { countUsers } = await db();
|
|
@@ -231,7 +231,7 @@ ${c.bold("Done.")} Start the dev server and open ${c.bold("/auth/sign-in")}.
|
|
|
231
231
|
Get the user in any route or server component:
|
|
232
232
|
|
|
233
233
|
import { requireUser } from "${PKG}/next"
|
|
234
|
-
const user = await requireUser() // { id,
|
|
234
|
+
const user = await requireUser() // { id, email }
|
|
235
235
|
|
|
236
236
|
Add users later: npx ${PKG} add-user
|
|
237
237
|
Or let people register themselves at /auth/sign-up:
|
|
@@ -245,8 +245,8 @@ async function main() {
|
|
|
245
245
|
console.log(`\n${c.bold(PKG)}\n`);
|
|
246
246
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
247
247
|
console.log(` npx ${PKG} set up auth in this project (safe to re-run)
|
|
248
|
-
npx ${PKG} add-user [
|
|
249
|
-
npx ${PKG} set-password [
|
|
248
|
+
npx ${PKG} add-user [email] create a user
|
|
249
|
+
npx ${PKG} set-password [email]
|
|
250
250
|
npx ${PKG} list-users
|
|
251
251
|
`);
|
|
252
252
|
return;
|
|
@@ -259,16 +259,16 @@ async function main() {
|
|
|
259
259
|
await createUserInteractive(arg);
|
|
260
260
|
if (cmd === "list-users") {
|
|
261
261
|
for (const u of await d.listUsers())
|
|
262
|
-
console.log(` ${u.
|
|
262
|
+
console.log(` ${u.email} ${c.dim(u.id)}`);
|
|
263
263
|
}
|
|
264
264
|
if (cmd === "set-password") {
|
|
265
|
-
const
|
|
265
|
+
const email = (arg ?? (await ask("Email"))).trim().toLowerCase();
|
|
266
266
|
const password = await ask("New password", { hidden: true });
|
|
267
267
|
if (password.length < 8)
|
|
268
268
|
fail("Password must be at least 8 characters.");
|
|
269
269
|
const { hashPassword } = await import("./core/password.js");
|
|
270
|
-
const ok = await d.setPassword(
|
|
271
|
-
ok ? done(`password updated for "${
|
|
270
|
+
const ok = await d.setPassword(email, await hashPassword(password));
|
|
271
|
+
ok ? done(`password updated for "${email}"`) : fail(`No user "${email}".`);
|
|
272
272
|
}
|
|
273
273
|
console.log();
|
|
274
274
|
return;
|
package/dist/core/db.d.ts
CHANGED
|
@@ -3,14 +3,14 @@ import type { User } from "./session.ts";
|
|
|
3
3
|
export declare function ensureSchema(): Promise<void>;
|
|
4
4
|
type Row = {
|
|
5
5
|
id: string;
|
|
6
|
-
|
|
6
|
+
email: string;
|
|
7
7
|
password_hash: string;
|
|
8
8
|
};
|
|
9
|
-
export declare function findUser(
|
|
10
|
-
export declare function createUser(
|
|
9
|
+
export declare function findUser(email: string): Promise<Row | null>;
|
|
10
|
+
export declare function createUser(email: string, passwordHash: string): Promise<User>;
|
|
11
11
|
export declare function countUsers(): Promise<number>;
|
|
12
12
|
export declare function listUsers(): Promise<(User & {
|
|
13
13
|
created_at: string;
|
|
14
14
|
})[]>;
|
|
15
|
-
export declare function setPassword(
|
|
15
|
+
export declare function setPassword(email: string, passwordHash: string): Promise<boolean>;
|
|
16
16
|
export { TABLE };
|
package/dist/core/db.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
2
|
// Storage. One table, owned by this package.
|
|
3
3
|
//
|
|
4
|
-
// auth_users (id,
|
|
4
|
+
// auth_users (id, email, password_hash, created_at)
|
|
5
5
|
//
|
|
6
6
|
// ensureSchema() is idempotent and memoized per process, so route
|
|
7
7
|
// handlers can call it on every request for free after the first.
|
|
@@ -18,7 +18,7 @@ export function ensureSchema() {
|
|
|
18
18
|
await sql() `
|
|
19
19
|
CREATE TABLE IF NOT EXISTS auth_users (
|
|
20
20
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
21
|
-
|
|
21
|
+
email TEXT NOT NULL UNIQUE,
|
|
22
22
|
password_hash TEXT NOT NULL,
|
|
23
23
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
24
24
|
)
|
|
@@ -28,19 +28,19 @@ export function ensureSchema() {
|
|
|
28
28
|
throw err;
|
|
29
29
|
}));
|
|
30
30
|
}
|
|
31
|
-
export async function findUser(
|
|
31
|
+
export async function findUser(email) {
|
|
32
32
|
await ensureSchema();
|
|
33
33
|
const rows = (await sql() `
|
|
34
|
-
SELECT id,
|
|
34
|
+
SELECT id, email, password_hash FROM auth_users WHERE email = ${email}
|
|
35
35
|
`);
|
|
36
36
|
return rows[0] ?? null;
|
|
37
37
|
}
|
|
38
|
-
export async function createUser(
|
|
38
|
+
export async function createUser(email, passwordHash) {
|
|
39
39
|
await ensureSchema();
|
|
40
40
|
const [row] = (await sql() `
|
|
41
|
-
INSERT INTO auth_users (
|
|
42
|
-
VALUES (${
|
|
43
|
-
RETURNING id,
|
|
41
|
+
INSERT INTO auth_users (email, password_hash)
|
|
42
|
+
VALUES (${email}, ${passwordHash})
|
|
43
|
+
RETURNING id, email
|
|
44
44
|
`);
|
|
45
45
|
return row;
|
|
46
46
|
}
|
|
@@ -52,13 +52,13 @@ export async function countUsers() {
|
|
|
52
52
|
export async function listUsers() {
|
|
53
53
|
await ensureSchema();
|
|
54
54
|
return (await sql() `
|
|
55
|
-
SELECT id,
|
|
55
|
+
SELECT id, email, created_at FROM auth_users ORDER BY created_at
|
|
56
56
|
`);
|
|
57
57
|
}
|
|
58
|
-
export async function setPassword(
|
|
58
|
+
export async function setPassword(email, passwordHash) {
|
|
59
59
|
await ensureSchema();
|
|
60
60
|
const rows = (await sql() `
|
|
61
|
-
UPDATE auth_users SET password_hash = ${passwordHash} WHERE
|
|
61
|
+
UPDATE auth_users SET password_hash = ${passwordHash} WHERE email = ${email} RETURNING id
|
|
62
62
|
`);
|
|
63
63
|
return rows.length > 0;
|
|
64
64
|
}
|
package/dist/core/html.js
CHANGED
|
@@ -53,7 +53,7 @@ export function signInPage(opts = {}) {
|
|
|
53
53
|
next: opts.next,
|
|
54
54
|
button: "Sign in",
|
|
55
55
|
fields: `
|
|
56
|
-
<label>
|
|
56
|
+
<label>Email <input name="email" type="email" autocomplete="email" autofocus required></label>
|
|
57
57
|
<label>Password <input name="password" type="password" autocomplete="current-password" required></label>`,
|
|
58
58
|
alt: opts.canSignUp ? `<p class="alt">No account? <a href="${SIGN_UP_PATH}${next}">Create one</a></p>` : undefined,
|
|
59
59
|
});
|
|
@@ -67,7 +67,7 @@ export function signUpPage(opts = {}) {
|
|
|
67
67
|
next: opts.next,
|
|
68
68
|
button: "Create account",
|
|
69
69
|
fields: `
|
|
70
|
-
<label>
|
|
70
|
+
<label>Email <input name="email" type="email" autocomplete="email" autofocus required></label>
|
|
71
71
|
<label>Password <input name="password" type="password" autocomplete="new-password" minlength="8" required></label>${opts.inviteRequired ? `\n <label>Invite code <input name="invite" autocomplete="off" required></label>` : ""}`,
|
|
72
72
|
alt: opts.first ? undefined : `<p class="alt">Have an account? <a href="${SIGN_IN_PATH}${next}">Sign in</a></p>`,
|
|
73
73
|
});
|
package/dist/core/session.d.ts
CHANGED
package/dist/core/session.js
CHANGED
|
@@ -9,7 +9,7 @@ import { SignJWT, jwtVerify } from "jose";
|
|
|
9
9
|
import { SESSION_DAYS } from "./config.js";
|
|
10
10
|
const key = (secret) => new TextEncoder().encode(secret);
|
|
11
11
|
export async function signSession(user, secret) {
|
|
12
|
-
return new SignJWT({
|
|
12
|
+
return new SignJWT({ email: user.email })
|
|
13
13
|
.setProtectedHeader({ alg: "HS256" })
|
|
14
14
|
.setSubject(user.id)
|
|
15
15
|
.setIssuedAt()
|
|
@@ -19,9 +19,9 @@ export async function signSession(user, secret) {
|
|
|
19
19
|
export async function verifySession(token, secret) {
|
|
20
20
|
try {
|
|
21
21
|
const { payload } = await jwtVerify(token, key(secret), { algorithms: ["HS256"] });
|
|
22
|
-
if (typeof payload.sub !== "string" || typeof payload.
|
|
22
|
+
if (typeof payload.sub !== "string" || typeof payload.email !== "string")
|
|
23
23
|
return null;
|
|
24
|
-
return { id: payload.sub,
|
|
24
|
+
return { id: payload.sub, email: payload.email };
|
|
25
25
|
}
|
|
26
26
|
catch {
|
|
27
27
|
return null;
|
package/dist/core/signup.d.ts
CHANGED
|
@@ -10,6 +10,6 @@ export declare function signupPolicy(env: {
|
|
|
10
10
|
AUTH_SIGNUP?: string;
|
|
11
11
|
AUTH_INVITE_CODE?: string;
|
|
12
12
|
}, userCount: number): SignupPolicy;
|
|
13
|
-
export declare const
|
|
13
|
+
export declare const EMAIL_RE: RegExp;
|
|
14
14
|
/** Returns an error message, or null when the pair is acceptable. */
|
|
15
|
-
export declare function validateCredentials(
|
|
15
|
+
export declare function validateCredentials(email: string, password: string): string | null;
|
package/dist/core/signup.js
CHANGED
|
@@ -15,11 +15,11 @@ export function signupPolicy(env, userCount) {
|
|
|
15
15
|
}
|
|
16
16
|
return { allowed: false, inviteRequired: false, reason: "closed" };
|
|
17
17
|
}
|
|
18
|
-
export const
|
|
18
|
+
export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
19
19
|
/** Returns an error message, or null when the pair is acceptable. */
|
|
20
|
-
export function validateCredentials(
|
|
21
|
-
if (!
|
|
22
|
-
return "
|
|
20
|
+
export function validateCredentials(email, password) {
|
|
21
|
+
if (!EMAIL_RE.test(email))
|
|
22
|
+
return "Please enter a valid email address.";
|
|
23
23
|
if (password.length < 8)
|
|
24
24
|
return "Password must be at least 8 characters.";
|
|
25
25
|
return null;
|
package/dist/next/index.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// GET /auth/session { user } or { user: null }
|
|
15
15
|
//
|
|
16
16
|
// Use anywhere on the server:
|
|
17
|
-
// const user = await requireUser() // { id,
|
|
18
|
-
// const user = await getUser() // { id,
|
|
17
|
+
// const user = await requireUser() // { id, email } or throws AuthError(401)
|
|
18
|
+
// const user = await getUser() // { id, email } or null
|
|
19
19
|
// ─────────────────────────────────────────────────────────────
|
|
20
20
|
import { cookies } from "next/headers.js";
|
|
21
21
|
import { NextResponse } from "next/server.js";
|
|
@@ -75,7 +75,7 @@ async function readBody(req) {
|
|
|
75
75
|
? (await req.json().catch(() => ({})))
|
|
76
76
|
: await req.formData();
|
|
77
77
|
return {
|
|
78
|
-
|
|
78
|
+
email: get(b, "email").trim().toLowerCase(),
|
|
79
79
|
password: get(b, "password"),
|
|
80
80
|
invite: get(b, "invite").trim(),
|
|
81
81
|
next: safeNext(get(b, "next")),
|
|
@@ -124,7 +124,7 @@ export async function GET(req, ctx) {
|
|
|
124
124
|
// A fresh install has nobody to sign in: go straight to creating the first account.
|
|
125
125
|
if (p.reason === "first")
|
|
126
126
|
return NextResponse.redirect(new URL(SIGN_UP_PATH, req.url));
|
|
127
|
-
const error = q.get("error") === "1" ? "Wrong
|
|
127
|
+
const error = q.get("error") === "1" ? "Wrong email or password." : (q.get("error") ?? undefined);
|
|
128
128
|
return html(signInPage({ next, error, canSignUp: p.allowed }));
|
|
129
129
|
}
|
|
130
130
|
if (route === "sign-up") {
|
|
@@ -147,30 +147,30 @@ export async function POST(req, ctx) {
|
|
|
147
147
|
if (route === "sign-out")
|
|
148
148
|
return signOutResponse(req, true);
|
|
149
149
|
if (route === "sign-in") {
|
|
150
|
-
const {
|
|
151
|
-
const row =
|
|
150
|
+
const { email, password, next, json } = await readBody(req);
|
|
151
|
+
const row = email && password ? await findUser(email) : null;
|
|
152
152
|
const ok = row ? await verifyPassword(password, row.password_hash) : false;
|
|
153
153
|
if (!row || !ok) {
|
|
154
154
|
if (json)
|
|
155
|
-
return NextResponse.json({ error: "Wrong
|
|
155
|
+
return NextResponse.json({ error: "Wrong email or password" }, { status: 401 });
|
|
156
156
|
return backTo(req, SIGN_IN_PATH, "1", next);
|
|
157
157
|
}
|
|
158
|
-
return signedIn(req, { id: row.id,
|
|
158
|
+
return signedIn(req, { id: row.id, email: row.email }, next, json);
|
|
159
159
|
}
|
|
160
160
|
if (route === "sign-up") {
|
|
161
|
-
const {
|
|
161
|
+
const { email, password, invite, next, json } = await readBody(req);
|
|
162
162
|
const p = await policy();
|
|
163
163
|
const fail = (msg, status = 400) => json ? NextResponse.json({ error: msg }, { status }) : backTo(req, SIGN_UP_PATH, msg, next);
|
|
164
164
|
if (!p.allowed)
|
|
165
165
|
return NextResponse.json({ error: "Sign-up is closed" }, { status: 404 });
|
|
166
166
|
if (p.inviteRequired && invite !== process.env.AUTH_INVITE_CODE)
|
|
167
167
|
return fail("Wrong invite code.", 403);
|
|
168
|
-
const invalid = validateCredentials(
|
|
168
|
+
const invalid = validateCredentials(email, password);
|
|
169
169
|
if (invalid)
|
|
170
170
|
return fail(invalid);
|
|
171
|
-
if (await findUser(
|
|
172
|
-
return fail("That
|
|
173
|
-
const user = await createUser(
|
|
171
|
+
if (await findUser(email))
|
|
172
|
+
return fail("That email is already registered.", 409);
|
|
173
|
+
const user = await createUser(email, await hashPassword(password));
|
|
174
174
|
return signedIn(req, user, next, json);
|
|
175
175
|
}
|
|
176
176
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@binary1702/auth",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Email + password auth for Next.js. One command: npx @binary1702/auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|