@binary1702/auth 0.1.0 → 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/INSTALL.md CHANGED
@@ -40,8 +40,19 @@ const user = await requireUser() // { id, username } or throws 401
40
40
 
41
41
  ## More users
42
42
 
43
+ From the terminal:
44
+
43
45
  ```bash
44
46
  npx @binary1702/auth add-user
45
47
  npx @binary1702/auth set-password
46
48
  npx @binary1702/auth list-users
47
49
  ```
50
+
51
+ Or let people register themselves at `/auth/sign-up`. Add to `.env.local`:
52
+
53
+ ```
54
+ AUTH_SIGNUP=open # anyone can create an account
55
+ AUTH_INVITE_CODE=secret # optional: the form then asks for this code
56
+ ```
57
+
58
+ Without `AUTH_SIGNUP`, sign-up is closed. The one exception is a fresh install with zero users, where `/auth/sign-in` takes you to create the first account.
package/PROMPT.md ADDED
@@ -0,0 +1,80 @@
1
+ # Agent prompt: install @binary1702/auth
2
+
3
+ Copy everything below the line into an AI coding agent running inside the Next.js project.
4
+
5
+ ---
6
+
7
+ Install `@binary1702/auth` in this project. It adds username + password auth to a Next.js App Router app. Follow these steps exactly and report the result of each.
8
+
9
+ ## Before you start
10
+
11
+ Check these and stop if any fails:
12
+
13
+ - `app/` or `src/app/` exists (App Router). Pages Router is not supported.
14
+ - A Postgres connection string is available. Look for `DATABASE_URL` or `NEON_DATABASE_URL` in `.env.local` or the environment. If none exists, ask me for one.
15
+ - If the project already has auth (a `proxy.ts` or `middleware.ts` that checks sessions, or packages like next-auth, better-auth, clerk, `@neondatabase/auth`), tell me before continuing. This package will put its own sign-in in front of everything.
16
+
17
+ ## Install
18
+
19
+ Run, in the project root:
20
+
21
+ ```bash
22
+ npx @binary1702/auth
23
+ ```
24
+
25
+ It is interactive. It asks for the Postgres connection string, then a username and a password for the first user. If you cannot answer prompts, run it and hand control to me.
26
+
27
+ It does the following, skipping anything already done:
28
+
29
+ ```
30
+ ✓ @binary1702/auth installed → package.json
31
+ ✓ DATABASE_URL → .env.local
32
+ ✓ AUTH_SECRET generated → .env.local
33
+ ✓ app/auth/[...path]/route.ts → export { GET, POST } from "@binary1702/auth/next"
34
+ ✓ proxy.ts → guard in front of every route
35
+ ✓ auth_users table ready → in the database
36
+ ✓ user "<name>" created
37
+ ```
38
+
39
+ If `proxy.ts` or `middleware.ts` already existed, the CLI does not touch it and prints a snippet instead. Merge the snippet into the existing file: call the package's `proxy` for the paths you want protected and keep the matcher inline. On Next 15 the file is `middleware.ts`, same content.
40
+
41
+ ## Verify
42
+
43
+ Start the dev server and check with real HTTP requests:
44
+
45
+ | Request | Expect |
46
+ |---|---|
47
+ | `GET /` with no cookie | redirect to `/auth/sign-in` |
48
+ | `GET /auth/sign-in` | 200, HTML form |
49
+ | `GET /api/<any>` with no cookie | 401 JSON |
50
+ | `POST /auth/sign-in` JSON `{ "username", "password" }` | 200 `{ user }` and `Set-Cookie: auth_session=…; HttpOnly` |
51
+ | `GET /auth/session` with that cookie | `{ user: { id, username } }` |
52
+ | `GET /auth/sign-out` | cookie cleared, redirect to sign-in |
53
+
54
+ Then open `/auth/sign-in` in the browser, sign in, confirm you land on `/`.
55
+
56
+ ## Wire it into the code
57
+
58
+ Wherever the app needs the current user on the server:
59
+
60
+ ```ts
61
+ import { requireUser } from "@binary1702/auth/next"
62
+
63
+ const user = await requireUser() // { id, username } or throws AuthError (401)
64
+ ```
65
+
66
+ `getUser()` returns `null` instead of throwing. Both work in route handlers, server components and server actions.
67
+
68
+ For sign-out, link to `/auth/sign-out`. For client code, `fetch("/auth/session")`.
69
+
70
+ Do not build a sign-up page. One exists at `/auth/sign-up`. It is closed by default (except for the very first user on an empty install). To let people register, add `AUTH_SIGNUP=open` to `.env.local`, optionally with `AUTH_INVITE_CODE=<code>` to require a code on the form. Users can also be created with `npx @binary1702/auth add-user`.
71
+
72
+ ## Deploy
73
+
74
+ Copy `DATABASE_URL` and `AUTH_SECRET` from `.env.local` into the host's environment. Nothing else.
75
+
76
+ ## Report
77
+
78
+ Tell me, in order: each install step and whether it was done or skipped, each verify row with the actual status code, anything that failed with the error text verbatim, and anything that took more steps than it should. Do not modify the package itself, only this project.
79
+
80
+ Docs: https://www.npmjs.com/package/@binary1702/auth
package/README.md CHANGED
@@ -8,6 +8,8 @@ npx @binary1702/auth
8
8
 
9
9
  One command. It installs itself, writes two env vars, writes two files, creates the table, creates your first user. Then you move on.
10
10
 
11
+ Letting an AI agent do the install? Paste [PROMPT.md](./PROMPT.md) into it. Same steps, written for an agent, with the checks to run and what to report back. [INSTALL.md](./INSTALL.md) is the short human version.
12
+
11
13
  ---
12
14
 
13
15
  ## Quick start (new project)
@@ -75,6 +77,7 @@ Works in route handlers, server components, and server actions.
75
77
  | URL | What it does |
76
78
  |---|---|
77
79
  | `/auth/sign-in` | The sign-in page. Served by the package, no React, no styling work. |
80
+ | `/auth/sign-up` | Self registration. Closed unless `AUTH_SIGNUP=open`. See [Users](#users). |
78
81
  | `/auth/sign-out` | Clears the session and sends you back to sign-in. Link to it. |
79
82
  | `/auth/session` | `{ user }` or `{ user: null }` as JSON, for client code. |
80
83
 
@@ -82,9 +85,11 @@ Everything else on the site requires a session. Unauthenticated page requests re
82
85
 
83
86
  ---
84
87
 
85
- ## Managing users
88
+ ## Users
89
+
90
+ Two ways in. Pick one or use both.
86
91
 
87
- There is no sign-up page. Users are created from the terminal.
92
+ **From the terminal** (always works):
88
93
 
89
94
  ```bash
90
95
  npx @binary1702/auth add-user # prompts for username and password
@@ -93,6 +98,18 @@ npx @binary1702/auth set-password ben
93
98
  npx @binary1702/auth list-users
94
99
  ```
95
100
 
101
+ **Self sign-up at `/auth/sign-up`**, controlled by one env var:
102
+
103
+ | `.env.local` | Who can sign up |
104
+ |---|---|
105
+ | nothing set | Nobody. Except on a fresh install with zero users: `/auth/sign-in` sends you to create the first account. |
106
+ | `AUTH_SIGNUP=open` | Anyone. The sign-in page gets a "Create one" link. |
107
+ | `AUTH_SIGNUP=open` + `AUTH_INVITE_CODE=secret` | Anyone who knows the code. The form gets an invite field. |
108
+
109
+ Rules on the form: username 2 to 32 characters (letters, numbers, `. _ -`), password at least 8. Accounts are created and signed in immediately. Nothing to approve.
110
+
111
+ Programmatic: `POST /auth/sign-up` as JSON `{ username, password, invite? }` returns `200 { user }` and the cookie, or `400 / 403 / 404 / 409` with `{ error }`.
112
+
96
113
  ---
97
114
 
98
115
  ## The two files it wrote
package/dist/cli.js CHANGED
@@ -234,6 +234,9 @@ Get the user in any route or server component:
234
234
  const user = await requireUser() // { id, username }
235
235
 
236
236
  Add users later: npx ${PKG} add-user
237
+ Or let people register themselves at /auth/sign-up:
238
+ AUTH_SIGNUP=open in .env.local (anyone)
239
+ AUTH_INVITE_CODE=<code> optional, the form then needs it
237
240
  `);
238
241
  }
239
242
  // ── main ──────────────────────────────────────────────────────
@@ -2,6 +2,7 @@ export declare const COOKIE_NAME = "auth_session";
2
2
  export declare const SESSION_DAYS = 7;
3
3
  export declare const SIGN_IN_PATH = "/auth/sign-in";
4
4
  export declare const SIGN_OUT_PATH = "/auth/sign-out";
5
+ export declare const SIGN_UP_PATH = "/auth/sign-up";
5
6
  export declare const TABLE = "auth_users";
6
7
  export declare function getSecret(): string;
7
8
  export declare function getDatabaseUrl(): string;
@@ -8,6 +8,7 @@ export const COOKIE_NAME = "auth_session";
8
8
  export const SESSION_DAYS = 7;
9
9
  export const SIGN_IN_PATH = "/auth/sign-in";
10
10
  export const SIGN_OUT_PATH = "/auth/sign-out";
11
+ export const SIGN_UP_PATH = "/auth/sign-up";
11
12
  export const TABLE = "auth_users";
12
13
  export function getSecret() {
13
14
  const secret = process.env.AUTH_SECRET;
@@ -2,4 +2,12 @@ export declare function signInPage(opts?: {
2
2
  error?: string;
3
3
  next?: string;
4
4
  title?: string;
5
+ canSignUp?: boolean;
6
+ }): string;
7
+ export declare function signUpPage(opts?: {
8
+ error?: string;
9
+ next?: string;
10
+ title?: string;
11
+ inviteRequired?: boolean;
12
+ first?: boolean;
5
13
  }): string;
package/dist/core/html.js CHANGED
@@ -1,20 +1,10 @@
1
1
  // ─────────────────────────────────────────────────────────────
2
- // The sign-in page. Plain HTML served by the route handler,
3
- // so the consumer never builds a form or a page.
2
+ // The sign-in and sign-up pages. Plain HTML served by the route
3
+ // handler, so the consumer never builds a form or a page.
4
4
  // ─────────────────────────────────────────────────────────────
5
- import { SIGN_IN_PATH } from "./config.js";
5
+ import { SIGN_IN_PATH, SIGN_UP_PATH } from "./config.js";
6
6
  const escape = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
7
- export function signInPage(opts = {}) {
8
- const title = escape(opts.title ?? "Sign in");
9
- const next = escape(opts.next ?? "/");
10
- const error = opts.error ? `<p class="error">${escape(opts.error)}</p>` : "";
11
- return `<!doctype html>
12
- <html lang="en">
13
- <head>
14
- <meta charset="utf-8">
15
- <meta name="viewport" content="width=device-width, initial-scale=1">
16
- <title>${title}</title>
17
- <style>
7
+ const STYLE = `
18
8
  :root { color-scheme: light dark; --fg: #111; --bg: #fff; --line: #ddd; --muted: #666; --err: #c0392b; }
19
9
  @media (prefers-color-scheme: dark) { :root { --fg: #eee; --bg: #111; --line: #333; --muted: #999; --err: #ff6b6b; } }
20
10
  * { box-sizing: border-box; }
@@ -27,17 +17,58 @@ export function signInPage(opts = {}) {
27
17
  input:focus { outline: 2px solid var(--fg); outline-offset: 1px; }
28
18
  button { font: inherit; font-weight: 500; padding: 9px; border: 0; border-radius: 6px; background: var(--fg); color: var(--bg); cursor: pointer; }
29
19
  .error { margin: 0; font-size: 13px; color: var(--err); }
30
- </style>
20
+ .alt { margin: 4px 0 0; font-size: 13px; color: var(--muted); text-align: center; }
21
+ .alt a { color: var(--fg); }
22
+ `;
23
+ function page(opts) {
24
+ const title = escape(opts.title);
25
+ const next = escape(opts.next ?? "/");
26
+ const error = opts.error ? `<p class="error">${escape(opts.error)}</p>` : "";
27
+ return `<!doctype html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="utf-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1">
32
+ <title>${title}</title>
33
+ <style>${STYLE}</style>
31
34
  </head>
32
35
  <body>
33
- <form method="post" action="${SIGN_IN_PATH}">
36
+ <form method="post" action="${opts.action}">
34
37
  <h1>${title}</h1>
35
38
  ${error}
36
- <label>Username <input name="username" autocomplete="username" autofocus required></label>
37
- <label>Password <input name="password" type="password" autocomplete="current-password" required></label>
39
+ ${opts.fields}
38
40
  <input type="hidden" name="next" value="${next}">
39
- <button type="submit">Sign in</button>
41
+ <button type="submit">${escape(opts.button)}</button>
42
+ ${opts.alt ?? ""}
40
43
  </form>
41
44
  </body>
42
45
  </html>`;
43
46
  }
47
+ export function signInPage(opts = {}) {
48
+ const next = opts.next && opts.next !== "/" ? `?next=${encodeURIComponent(opts.next)}` : "";
49
+ return page({
50
+ title: opts.title ?? "Sign in",
51
+ action: SIGN_IN_PATH,
52
+ error: opts.error,
53
+ next: opts.next,
54
+ button: "Sign in",
55
+ fields: `
56
+ <label>Username <input name="username" autocomplete="username" autofocus required></label>
57
+ <label>Password <input name="password" type="password" autocomplete="current-password" required></label>`,
58
+ alt: opts.canSignUp ? `<p class="alt">No account? <a href="${SIGN_UP_PATH}${next}">Create one</a></p>` : undefined,
59
+ });
60
+ }
61
+ export function signUpPage(opts = {}) {
62
+ const next = opts.next && opts.next !== "/" ? `?next=${encodeURIComponent(opts.next)}` : "";
63
+ return page({
64
+ title: opts.title ?? (opts.first ? "Create the first account" : "Create account"),
65
+ action: SIGN_UP_PATH,
66
+ error: opts.error,
67
+ next: opts.next,
68
+ button: "Create account",
69
+ fields: `
70
+ <label>Username <input name="username" autocomplete="username" autofocus required></label>
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
+ alt: opts.first ? undefined : `<p class="alt">Have an account? <a href="${SIGN_IN_PATH}${next}">Sign in</a></p>`,
73
+ });
74
+ }
@@ -0,0 +1,15 @@
1
+ export type SignupPolicy = {
2
+ /** Can anyone reach the sign-up page right now? */
3
+ allowed: boolean;
4
+ /** Is an invite code required on the form? */
5
+ inviteRequired: boolean;
6
+ /** Why it is allowed. "first" skips the invite code: there is nobody to invite you. */
7
+ reason: "open" | "first" | "closed";
8
+ };
9
+ export declare function signupPolicy(env: {
10
+ AUTH_SIGNUP?: string;
11
+ AUTH_INVITE_CODE?: string;
12
+ }, userCount: number): SignupPolicy;
13
+ export declare const USERNAME_RE: RegExp;
14
+ /** Returns an error message, or null when the pair is acceptable. */
15
+ export declare function validateCredentials(username: string, password: string): string | null;
@@ -0,0 +1,26 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Sign-up policy. One pure function, so it can be tested and read.
3
+ //
4
+ // AUTH_SIGNUP=open anyone may register at /auth/sign-up
5
+ // AUTH_INVITE_CODE=<code> with "open": the form also needs this code
6
+ // (unset) closed, EXCEPT when there are no users yet,
7
+ // so a fresh install can create its first
8
+ // account in the browser instead of the CLI
9
+ // ─────────────────────────────────────────────────────────────
10
+ export function signupPolicy(env, userCount) {
11
+ if (userCount === 0)
12
+ return { allowed: true, inviteRequired: false, reason: "first" };
13
+ if (env.AUTH_SIGNUP === "open") {
14
+ return { allowed: true, inviteRequired: Boolean(env.AUTH_INVITE_CODE), reason: "open" };
15
+ }
16
+ return { allowed: false, inviteRequired: false, reason: "closed" };
17
+ }
18
+ export const USERNAME_RE = /^[a-z0-9][a-z0-9._-]{1,31}$/i;
19
+ /** Returns an error message, or null when the pair is acceptable. */
20
+ export function validateCredentials(username, password) {
21
+ if (!USERNAME_RE.test(username))
22
+ return "Username: 2 to 32 letters, numbers, dots, dashes or underscores.";
23
+ if (password.length < 8)
24
+ return "Password must be at least 8 characters.";
25
+ return null;
26
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { hashPassword, verifyPassword } from "./core/password.ts";
2
2
  export { signSession, verifySession, type User } from "./core/session.ts";
3
3
  export { ensureSchema, findUser, createUser, countUsers, listUsers, setPassword } from "./core/db.ts";
4
- export { signInPage } from "./core/html.ts";
5
- export { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, SIGN_OUT_PATH, getSecret, getDatabaseUrl } from "./core/config.ts";
4
+ export { signInPage, signUpPage } from "./core/html.ts";
5
+ export { signupPolicy, validateCredentials, type SignupPolicy } from "./core/signup.ts";
6
+ export { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, SIGN_OUT_PATH, SIGN_UP_PATH, getSecret, getDatabaseUrl } from "./core/config.ts";
package/dist/index.js CHANGED
@@ -3,5 +3,6 @@
3
3
  export { hashPassword, verifyPassword } from "./core/password.js";
4
4
  export { signSession, verifySession } from "./core/session.js";
5
5
  export { ensureSchema, findUser, createUser, countUsers, listUsers, setPassword } from "./core/db.js";
6
- export { signInPage } from "./core/html.js";
7
- export { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, SIGN_OUT_PATH, getSecret, getDatabaseUrl } from "./core/config.js";
6
+ export { signInPage, signUpPage } from "./core/html.js";
7
+ export { signupPolicy, validateCredentials } from "./core/signup.js";
8
+ export { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, SIGN_OUT_PATH, SIGN_UP_PATH, getSecret, getDatabaseUrl } from "./core/config.js";
@@ -7,6 +7,8 @@
7
7
  // Serves:
8
8
  // GET /auth/sign-in the sign-in page
9
9
  // POST /auth/sign-in verify, set cookie, redirect to ?next or /
10
+ // GET /auth/sign-up the sign-up page (see core/signup.ts for when)
11
+ // POST /auth/sign-up create user, set cookie, redirect
10
12
  // GET /auth/sign-out clear cookie, redirect to sign-in
11
13
  // POST /auth/sign-out same (for fetch callers)
12
14
  // GET /auth/session { user } or { user: null }
@@ -17,11 +19,12 @@
17
19
  // ─────────────────────────────────────────────────────────────
18
20
  import { cookies } from "next/headers.js";
19
21
  import { NextResponse } from "next/server.js";
20
- import { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, getSecret } from "../core/config.js";
22
+ import { COOKIE_NAME, SESSION_DAYS, SIGN_IN_PATH, SIGN_UP_PATH, getSecret } from "../core/config.js";
21
23
  import { signSession, verifySession } from "../core/session.js";
22
- import { verifyPassword } from "../core/password.js";
23
- import { findUser } from "../core/db.js";
24
- import { signInPage } from "../core/html.js";
24
+ import { hashPassword, verifyPassword } from "../core/password.js";
25
+ import { countUsers, createUser, findUser } from "../core/db.js";
26
+ import { signInPage, signUpPage } from "../core/html.js";
27
+ import { signupPolicy, validateCredentials } from "../core/signup.js";
25
28
  export class AuthError extends Error {
26
29
  status = 401;
27
30
  constructor(message = "Unauthorized") {
@@ -67,18 +70,37 @@ function html(body, status = 200) {
67
70
  }
68
71
  async function readBody(req) {
69
72
  const type = req.headers.get("content-type") ?? "";
70
- if (type.includes("application/json")) {
71
- const b = (await req.json().catch(() => ({})));
72
- return { username: String(b.username ?? ""), password: String(b.password ?? ""), next: safeNext(String(b.next ?? "")), json: true };
73
- }
74
- const f = await req.formData();
73
+ const get = (b, k) => String((b instanceof FormData ? b.get(k) : b[k]) ?? "");
74
+ const b = type.includes("application/json")
75
+ ? (await req.json().catch(() => ({})))
76
+ : await req.formData();
75
77
  return {
76
- username: String(f.get("username") ?? ""),
77
- password: String(f.get("password") ?? ""),
78
- next: safeNext(String(f.get("next") ?? "")),
79
- json: false,
78
+ username: get(b, "username").trim(),
79
+ password: get(b, "password"),
80
+ invite: get(b, "invite").trim(),
81
+ next: safeNext(get(b, "next")),
82
+ json: type.includes("application/json"),
80
83
  };
81
84
  }
85
+ async function currentUser(req) {
86
+ const token = req.cookies.get(COOKIE_NAME)?.value;
87
+ return token ? verifySession(token, getSecret()) : null;
88
+ }
89
+ /** Sign the user in on a response: cookie + redirect (form) or JSON (fetch). */
90
+ async function signedIn(req, user, next, json) {
91
+ const token = await signSession(user, getSecret());
92
+ const res = json ? NextResponse.json({ user }) : NextResponse.redirect(new URL(next, req.url), 303);
93
+ res.cookies.set(COOKIE_NAME, token, cookieOptions());
94
+ return res;
95
+ }
96
+ /** Back to a form with ?error=<msg>, keeping ?next. */
97
+ function backTo(req, path, error, next) {
98
+ const url = new URL(path, req.url);
99
+ url.searchParams.set("error", error);
100
+ if (next !== "/")
101
+ url.searchParams.set("next", next);
102
+ return NextResponse.redirect(url, 303);
103
+ }
82
104
  function signOutResponse(req, json) {
83
105
  const res = json
84
106
  ? NextResponse.json({ ok: true })
@@ -86,26 +108,37 @@ function signOutResponse(req, json) {
86
108
  res.cookies.set(COOKIE_NAME, "", { ...cookieOptions(), maxAge: 0 });
87
109
  return res;
88
110
  }
111
+ async function policy() {
112
+ return signupPolicy(process.env, await countUsers());
113
+ }
89
114
  // ── handlers ──────────────────────────────────────────────────
90
115
  export async function GET(req, ctx) {
91
116
  const { path } = await ctx.params;
92
117
  const route = path.join("/");
118
+ const q = req.nextUrl.searchParams;
119
+ const next = safeNext(q.get("next"));
93
120
  if (route === "sign-in") {
94
- const next = safeNext(req.nextUrl.searchParams.get("next"));
95
- const existing = req.cookies.get(COOKIE_NAME)?.value;
96
- if (existing && (await verifySession(existing, getSecret()))) {
121
+ if (await currentUser(req))
97
122
  return NextResponse.redirect(new URL(next, req.url));
98
- }
99
- const error = req.nextUrl.searchParams.get("error") ? "Wrong username or password." : undefined;
100
- return html(signInPage({ next, error }));
123
+ const p = await policy();
124
+ // A fresh install has nobody to sign in: go straight to creating the first account.
125
+ if (p.reason === "first")
126
+ return NextResponse.redirect(new URL(SIGN_UP_PATH, req.url));
127
+ const error = q.get("error") === "1" ? "Wrong username or password." : (q.get("error") ?? undefined);
128
+ return html(signInPage({ next, error, canSignUp: p.allowed }));
129
+ }
130
+ if (route === "sign-up") {
131
+ if (await currentUser(req))
132
+ return NextResponse.redirect(new URL(next, req.url));
133
+ const p = await policy();
134
+ if (!p.allowed)
135
+ return NextResponse.json({ error: "Sign-up is closed" }, { status: 404 });
136
+ return html(signUpPage({ next, error: q.get("error") ?? undefined, inviteRequired: p.inviteRequired, first: p.reason === "first" }));
101
137
  }
102
138
  if (route === "sign-out")
103
139
  return signOutResponse(req, false);
104
- if (route === "session") {
105
- const token = req.cookies.get(COOKIE_NAME)?.value;
106
- const user = token ? await verifySession(token, getSecret()) : null;
107
- return NextResponse.json({ user });
108
- }
140
+ if (route === "session")
141
+ return NextResponse.json({ user: await currentUser(req) });
109
142
  return NextResponse.json({ error: "Not found" }, { status: 404 });
110
143
  }
111
144
  export async function POST(req, ctx) {
@@ -113,23 +146,32 @@ export async function POST(req, ctx) {
113
146
  const route = path.join("/");
114
147
  if (route === "sign-out")
115
148
  return signOutResponse(req, true);
116
- if (route !== "sign-in")
117
- return NextResponse.json({ error: "Not found" }, { status: 404 });
118
- const { username, password, next, json } = await readBody(req);
119
- const row = username && password ? await findUser(username.trim()) : null;
120
- const ok = row ? await verifyPassword(password, row.password_hash) : false;
121
- if (!row || !ok) {
122
- if (json)
123
- return NextResponse.json({ error: "Wrong username or password" }, { status: 401 });
124
- const url = new URL(SIGN_IN_PATH, req.url);
125
- url.searchParams.set("error", "1");
126
- if (next !== "/")
127
- url.searchParams.set("next", next);
128
- return NextResponse.redirect(url, 303);
149
+ if (route === "sign-in") {
150
+ const { username, password, next, json } = await readBody(req);
151
+ const row = username && password ? await findUser(username) : null;
152
+ const ok = row ? await verifyPassword(password, row.password_hash) : false;
153
+ if (!row || !ok) {
154
+ if (json)
155
+ return NextResponse.json({ error: "Wrong username or password" }, { status: 401 });
156
+ return backTo(req, SIGN_IN_PATH, "1", next);
157
+ }
158
+ return signedIn(req, { id: row.id, username: row.username }, next, json);
129
159
  }
130
- const user = { id: row.id, username: row.username };
131
- const token = await signSession(user, getSecret());
132
- const res = json ? NextResponse.json({ user }) : NextResponse.redirect(new URL(next, req.url), 303);
133
- res.cookies.set(COOKIE_NAME, token, cookieOptions());
134
- return res;
160
+ if (route === "sign-up") {
161
+ const { username, password, invite, next, json } = await readBody(req);
162
+ const p = await policy();
163
+ const fail = (msg, status = 400) => json ? NextResponse.json({ error: msg }, { status }) : backTo(req, SIGN_UP_PATH, msg, next);
164
+ if (!p.allowed)
165
+ return NextResponse.json({ error: "Sign-up is closed" }, { status: 404 });
166
+ if (p.inviteRequired && invite !== process.env.AUTH_INVITE_CODE)
167
+ return fail("Wrong invite code.", 403);
168
+ const invalid = validateCredentials(username, password);
169
+ if (invalid)
170
+ return fail(invalid);
171
+ if (await findUser(username))
172
+ return fail("That username is taken.", 409);
173
+ const user = await createUser(username, await hashPassword(password));
174
+ return signedIn(req, user, next, json);
175
+ }
176
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
135
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@binary1702/auth",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Username + password auth for Next.js. One command: npx @binary1702/auth",
5
5
  "type": "module",
6
6
  "repository": {
@@ -30,7 +30,8 @@
30
30
  },
31
31
  "files": [
32
32
  "dist",
33
- "INSTALL.md"
33
+ "INSTALL.md",
34
+ "PROMPT.md"
34
35
  ],
35
36
  "scripts": {
36
37
  "build": "tsc",