@binary1702/auth 0.3.0 → 0.4.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.
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { type Authorization, type Organization, type Session } from "../core/org.ts";
3
- export type { Session, Organization, Authorization };
3
+ import type { SignupPolicy } from "../core/signup.ts";
4
+ export type { Session, Organization, Authorization, SignupPolicy };
4
5
  export type AuthState = {
5
6
  user: Session["user"] | null;
6
7
  organization: Organization | null;
@@ -9,6 +10,8 @@ export type AuthState = {
9
10
  loading: boolean;
10
11
  /** Re-fetch GET /auth/session (after switching organization, for instance) */
11
12
  refresh: () => Promise<void>;
13
+ /** May this user do that? For showing and hiding UI; the server still checks with requirePermission(). */
14
+ can: (permission: string) => boolean;
12
15
  };
13
16
  export declare function fetchSession(): Promise<Session | null>;
14
17
  export declare function AuthProvider(props: {
@@ -16,3 +19,54 @@ export declare function AuthProvider(props: {
16
19
  children: ReactNode;
17
20
  }): import("react").FunctionComponentElement<import("react").ProviderProps<AuthState | null>>;
18
21
  export declare function useAuth(): AuthState;
22
+ /**
23
+ * What a sign-in or sign-up attempt comes back with.
24
+ * ok, verify false signed in; go to `next` (checked by the server: same origin only)
25
+ * ok, verify true account created, no session yet: "check your email"
26
+ * not ok `error` is the message to show; `status` 401 wrong password,
27
+ * 403 cross-site / wrong invite / email not verified, 404 sign-up closed,
28
+ * 409 email taken, 429 too many tries (`retryAfter` seconds)
29
+ */
30
+ export type AuthResult = {
31
+ ok: true;
32
+ user: Session["user"];
33
+ next: string;
34
+ verify: boolean;
35
+ } | {
36
+ ok: false;
37
+ status: number;
38
+ error: string;
39
+ verify: boolean;
40
+ retryAfter?: number;
41
+ };
42
+ /** POST /auth/sign-in. On success the cookie is set; navigate to `next`. */
43
+ export declare function signIn(input: {
44
+ email: string;
45
+ password: string;
46
+ next?: string;
47
+ }): Promise<AuthResult>;
48
+ /** POST /auth/sign-up. `invite` when the policy requires it. */
49
+ export declare function signUp(input: {
50
+ email: string;
51
+ password: string;
52
+ invite?: string;
53
+ next?: string;
54
+ }): Promise<AuthResult>;
55
+ /** POST /auth/sign-out: clears the cookie. Then navigate somewhere public (or link to /auth/sign-out?next=/). */
56
+ export declare function signOut(): Promise<void>;
57
+ /** What a sign-in or sign-up page needs to know before it renders. */
58
+ export type Policy = {
59
+ /** May anyone sign up right now, is an invite code required, and is this the first account? */
60
+ signup: SignupPolicy;
61
+ /** Where the pages are: the app's own (b17_auth.yaml `pages:`) or the package's */
62
+ pages: {
63
+ signIn: string;
64
+ signUp: string;
65
+ };
66
+ /** The signed-in home (b17_auth.yaml `home:`), else "/" */
67
+ home: string;
68
+ /** The app name from b17_auth.yaml, if set */
69
+ name?: string;
70
+ };
71
+ /** GET /auth/policy */
72
+ export declare function fetchPolicy(): Promise<Policy>;
@@ -10,10 +10,23 @@
10
10
  // const { user, organization, authorization } = useAuth()
11
11
  //
12
12
  // Signed out: user and organization are null, permissions are empty.
13
+ //
14
+ // For an app that draws its own sign-in / sign-up pages (b17_auth.yaml
15
+ // `pages:`), the protocol as functions. The package decides what is
16
+ // valid, where to go next and when to refuse; the page only renders:
17
+ //
18
+ // const r = await signIn({ email, password, next })
19
+ // r.ok ? location.assign(r.next) : setError(r.error)
20
+ //
21
+ // const { signup } = await fetchPolicy() // may anyone sign up right now? first account?
13
22
  // ─────────────────────────────────────────────────────────────
14
23
  import { createContext, createElement, useCallback, useContext, useEffect, useState } from "react";
15
- import { NO_SESSION } from "../core/org.js";
24
+ import { NO_SESSION, hasPermission } from "../core/org.js";
16
25
  const SESSION_URL = "/auth/session";
26
+ const SIGN_IN_URL = "/auth/sign-in";
27
+ const SIGN_UP_URL = "/auth/sign-up";
28
+ const SIGN_OUT_URL = "/auth/sign-out";
29
+ const POLICY_URL = "/auth/policy";
17
30
  const Ctx = createContext(null);
18
31
  export async function fetchSession() {
19
32
  const res = await fetch(SESSION_URL, { credentials: "same-origin", cache: "no-store" });
@@ -38,12 +51,14 @@ export function AuthProvider(props) {
38
51
  else
39
52
  void refresh();
40
53
  }, [provided, props.session, refresh]);
54
+ const authorization = session?.authorization ?? NO_SESSION.authorization;
41
55
  const value = {
42
56
  user: session?.user ?? NO_SESSION.user,
43
57
  organization: session?.organization ?? NO_SESSION.organization,
44
- authorization: session?.authorization ?? NO_SESSION.authorization,
58
+ authorization,
45
59
  loading,
46
60
  refresh,
61
+ can: (permission) => hasPermission({ authorization }, permission),
47
62
  };
48
63
  return createElement(Ctx.Provider, { value }, props.children);
49
64
  }
@@ -53,3 +68,40 @@ export function useAuth() {
53
68
  throw new Error("useAuth() needs an <AuthProvider> above it");
54
69
  return ctx;
55
70
  }
71
+ async function attempt(url, body) {
72
+ const res = await fetch(url, {
73
+ method: "POST",
74
+ credentials: "same-origin",
75
+ headers: { "content-type": "application/json" },
76
+ body: JSON.stringify(body),
77
+ });
78
+ const data = (await res.json().catch(() => ({})));
79
+ if (res.ok && data.user)
80
+ return { ok: true, user: data.user, next: data.next ?? "/", verify: data.verify === true };
81
+ return {
82
+ ok: false,
83
+ status: res.status,
84
+ error: data.error ?? (data.verify ? "Check your email for a verification link, then sign in." : "Something went wrong. Try again."),
85
+ verify: data.verify === true,
86
+ retryAfter: data.retryAfter,
87
+ };
88
+ }
89
+ /** POST /auth/sign-in. On success the cookie is set; navigate to `next`. */
90
+ export function signIn(input) {
91
+ return attempt(SIGN_IN_URL, input);
92
+ }
93
+ /** POST /auth/sign-up. `invite` when the policy requires it. */
94
+ export function signUp(input) {
95
+ return attempt(SIGN_UP_URL, input);
96
+ }
97
+ /** POST /auth/sign-out: clears the cookie. Then navigate somewhere public (or link to /auth/sign-out?next=/). */
98
+ export async function signOut() {
99
+ await fetch(SIGN_OUT_URL, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: "{}" });
100
+ }
101
+ /** GET /auth/policy */
102
+ export async function fetchPolicy() {
103
+ const res = await fetch(POLICY_URL, { credentials: "same-origin", cache: "no-store" });
104
+ if (!res.ok)
105
+ throw new Error(`GET ${POLICY_URL} answered ${res.status}`);
106
+ return (await res.json());
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@binary1702/auth",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Email + password auth for Next.js. One command: npx @binary1702/auth",
5
5
  "type": "module",
6
6
  "repository": {
@@ -42,7 +42,8 @@
42
42
  "scripts": {
43
43
  "build": "tsc",
44
44
  "prepublishOnly": "npm run build",
45
- "test": "node --test tests/*.test.ts"
45
+ "test": "node --test tests/*.test.ts",
46
+ "dev": "npx next dev demo -p 3002"
46
47
  },
47
48
  "dependencies": {
48
49
  "@neondatabase/serverless": "^1.1.0",