@merijnslegers/admin 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/README.md CHANGED
@@ -52,7 +52,12 @@ export const admin = createAdmin({
52
52
  serviceRoleKey: SUPABASE_SERVICE_ROLE_KEY,
53
53
  allowList: ADMIN_EMAILS,
54
54
  // Optional: a project in Dutch names its screens in Dutch.
55
- paths: { login: '/admin/inloggen', password: '/admin/wachtwoord' }
55
+ paths: { login: '/admin/inloggen', password: '/admin/wachtwoord' },
56
+ // Optional: the hook's 401 and 403 in the project's own words.
57
+ responses: {
58
+ unauthorized: 'U bent niet ingelogd.',
59
+ forbidden: 'Dit verzoek kwam niet van onze eigen site.'
60
+ }
56
61
  });
57
62
  ```
58
63
 
@@ -74,6 +79,8 @@ For everything under the admin path it:
74
79
  - answers a request that can change something with a 403 when it does not come
75
80
  from the site's own origin, even when SvelteKit's own check is off for a
76
81
  webhook;
82
+ - gives that 401 or 403 the text from `responses` as plain text, or an empty
83
+ body without it, so the package chooses no words of its own;
77
84
  - puts `ADMIN_HEADERS` on every response: no framing, `noindex`, `no-store`,
78
85
  a referrer that stays on the site.
79
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merijnslegers/admin",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The admin login of the software factory: Supabase Auth with an allow list, the access rules and headers, and image resizing in the browser.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/rules.d.ts CHANGED
@@ -13,6 +13,12 @@ export type AdminDecision =
13
13
  | { kind: 'unauthorized' }
14
14
  | { kind: 'forbidden' };
15
15
 
16
+ /** The project's own words for the hook's 401 and 403. A missing one is an empty body. */
17
+ export interface AdminResponses {
18
+ unauthorized?: string;
19
+ forbidden?: string;
20
+ }
21
+
16
22
  export interface SettingNames {
17
23
  supabaseUrl: string;
18
24
  anonKey: string;
@@ -45,6 +51,7 @@ export function adminDecision(request: {
45
51
  signedIn: boolean;
46
52
  paths: AdminPaths;
47
53
  }): AdminDecision;
54
+ export function refusal(kind: 'unauthorized' | 'forbidden', responses?: AdminResponses): Response;
48
55
  export function missingSettings(
49
56
  settings: {
50
57
  supabaseUrl: string;
package/src/rules.js CHANGED
@@ -153,6 +153,20 @@ export function adminDecision({ url, method, contentType, origin, signedIn, path
153
153
  return { kind: 'unauthorized' };
154
154
  }
155
155
 
156
+ /**
157
+ * The answer to an `unauthorized` (401) or `forbidden` (403) decision. The
158
+ * package chooses no words (F-0011): the body is the project's own text from
159
+ * `responses`, or empty when it passes none. `ADMIN_HEADERS` are on it too.
160
+ */
161
+ export function refusal(kind, responses = {}) {
162
+ const status = kind === 'forbidden' ? 403 : 401;
163
+ const text = (kind === 'forbidden' ? responses.forbidden : responses.unauthorized) ?? '';
164
+ const headers = new Headers(ADMIN_HEADERS);
165
+ if (!text) return new Response(null, { status, headers });
166
+ headers.set('Content-Type', 'text/plain; charset=utf-8');
167
+ return new Response(text, { status, headers });
168
+ }
169
+
156
170
  /**
157
171
  * The settings logging in needs that are missing, by the names the project
158
172
  * gave them. Empty when everything is there. Without them nobody gets in
package/src/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Cookies } from '@sveltejs/kit';
2
2
  import type { Handle } from '@sveltejs/kit/hooks';
3
- import type { AdminPaths, SettingNames } from './rules.js';
3
+ import type { AdminPaths, AdminResponses, SettingNames } from './rules.js';
4
4
 
5
5
  export interface AdminOptions {
6
6
  /** Read at runtime in the project's src/env.ts (F-0005). An empty string means missing. */
@@ -13,6 +13,8 @@ export interface AdminOptions {
13
13
  paths?: Partial<AdminPaths>;
14
14
  /** The names `missingSettings()` reports, if the project's variables are named differently. */
15
15
  settingNames?: Partial<SettingNames>;
16
+ /** The body of the hook's 401 and 403, in the project's own words. Without it the body is empty. */
17
+ responses?: AdminResponses;
16
18
  }
17
19
 
18
20
  export interface Admin {
package/src/server.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  parseAllowList,
25
25
  passwordOutcome,
26
26
  passwordProblem,
27
+ refusal,
27
28
  signInOutcome
28
29
  } from './rules.js';
29
30
 
@@ -106,9 +107,7 @@ export function createAdmin(options) {
106
107
  const response =
107
108
  decision.kind === 'pass'
108
109
  ? await resolve(event)
109
- : new Response(decision.kind === 'forbidden' ? 'Forbidden' : 'Unauthorized', {
110
- status: decision.kind === 'forbidden' ? 403 : 401
111
- });
110
+ : refusal(decision.kind, options.responses);
112
111
  for (const [name, value] of Object.entries(ADMIN_HEADERS)) response.headers.set(name, value);
113
112
  return response;
114
113
  }