@server/next 0.20.50 → 0.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.20.50",
3
+ "version": "0.21.1",
4
4
  "description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
5
5
  "homepage": "https://server-js.com/",
6
6
  "repository": "https://github.com/franciscop/server-next.git",
@@ -10,8 +10,9 @@
10
10
  "license": "UNLICENSED",
11
11
  "documentation": {
12
12
  "title": "Server JS - A modern web server for Bun and Node.js",
13
+ "home": "./docs/index.html",
13
14
  "menu": {
14
- "About": "/about",
15
+ "About": "/",
15
16
  "Documentation": "/documentation",
16
17
  "Github": "https://github.com/franciscop/server-next"
17
18
  }
@@ -34,11 +35,10 @@
34
35
  "index.d.ts",
35
36
  "jsx-dev-runtime.js"
36
37
  ],
37
- "dependencies": {},
38
38
  "devDependencies": {
39
39
  "argon2": "^0.40.3",
40
40
  "jest": "^29.7.0",
41
- "polystore": "^0.8.0"
41
+ "polystore": "^0.14.1"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=20.0.0"
package/src/auth/auth.js CHANGED
@@ -5,14 +5,14 @@ const validateToken = (authorization) => {
5
5
  if (type.toLowerCase() !== "bearer") {
6
6
  throw ServerError.AUTH_INVALID_TYPE({ type });
7
7
  }
8
- if (id.length !== 24) {
8
+ if (id.length !== 16) {
9
9
  throw ServerError.AUTH_INVALID_TOKEN();
10
10
  }
11
11
  return id;
12
12
  };
13
13
 
14
14
  const validateCookie = (authorization) => {
15
- if (authorization.length !== 24) {
15
+ if (authorization.length !== 16) {
16
16
  throw ServerError.AUTH_INVALID_COOKIE();
17
17
  }
18
18
  return authorization;
@@ -31,15 +31,15 @@ const findSessionId = (ctx) => {
31
31
 
32
32
  if (type === "cookie") {
33
33
  // If the user is not authenticated, there's no auth to retrieve
34
- if (!ctx.cookies.authorization) return;
34
+ if (!ctx.cookies.authentication) return;
35
35
 
36
- return validateCookie(ctx.cookies.authorization);
36
+ return validateCookie(ctx.cookies.authentication);
37
37
  }
38
38
 
39
- throw new Error("Invalid auth type " + type);
39
+ throw new Error(`Invalid auth type "${type}"`);
40
40
  };
41
41
 
42
- export default async function findAuth(ctx) {
42
+ export default async function auth(ctx) {
43
43
  if (!ctx.options.auth) return; // NO AUTH AT ALL; nothing to do here
44
44
  const options = ctx.options.auth;
45
45
 
@@ -47,7 +47,10 @@ export default async function findAuth(ctx) {
47
47
  if (!sessionId) return; // NO SESSION FOUND; no auth
48
48
 
49
49
  const auth = await options.session.get(sessionId);
50
+ // Mmh, which one to do...
50
51
  if (!auth) throw ServerError.AUTH_NO_SESSION();
52
+ // if (!auth) return; // SESSION ALREADY INVALID; no auth
53
+
51
54
  if (!auth.provider) throw ServerError.AUTH_NO_PROVIDER();
52
55
  if (!options.provider.includes(auth.provider)) {
53
56
  const valid = JSON.stringify(options.provider);
package/src/auth/index.js CHANGED
@@ -14,6 +14,13 @@ const middle = async (ctx) => {
14
14
  if (ctx.options.auth) {
15
15
  ctx.app.post("/auth/logout", logout);
16
16
 
17
+ if (ctx.options.auth.provider.includes("github")) {
18
+ if (!env.GITHUB_ID) throw new Error("GITHUB_ID not defined");
19
+ if (!env.GITHUB_SECRET) throw new Error("GITHUB_SECRET not defined");
20
+ ctx.app.get("/auth/login/github", providers.github.login);
21
+ ctx.app.get("/auth/callback/github", providers.github.callback);
22
+ }
23
+
17
24
  if (ctx.options.auth.provider.includes("email")) {
18
25
  ctx.app.post("/auth/register/email", providers.email.register);
19
26
  ctx.app.post("/auth/login/email", providers.email.login);
@@ -4,7 +4,7 @@ import kv from "polystore";
4
4
 
5
5
  import server from "../index.js";
6
6
 
7
- const ID = "REqA2l022l8Q0tuIRtqLOPUy";
7
+ const ID = "REqA2l022l8Q0tuI";
8
8
 
9
9
  describe("auth", () => {
10
10
  it("requires a provider", () => {
@@ -24,14 +24,14 @@ describe("auth", () => {
24
24
  id: ID,
25
25
  type: "token",
26
26
  provider: "wrong",
27
- user: "QypOn5SQApyOPdUpIZsO9u2O",
27
+ user: "QypOn5SQApyOPdUp",
28
28
  email: "abc@test.com",
29
29
  time: "2024-07-01T03:21:40Z",
30
30
  });
31
- const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
31
+ const authorization = "Bearer REqA2l022l8Q0tuI";
32
32
  const req = await api.get("/", { headers: { authorization } });
33
33
  expect(req).not.toSucceed(
34
- 'Invalid provider "wrong", valid ones are: "email"'
34
+ 'Invalid provider "wrong", valid ones are: "email"',
35
35
  );
36
36
  });
37
37
  });
@@ -47,7 +47,7 @@ describe("token", () => {
47
47
  });
48
48
 
49
49
  it("should be Bearer", async () => {
50
- const authorization = "Basic REqA2l022l8Q0tuIRtqLOPUy";
50
+ const authorization = "Basic REqA2l022l8Q0tuI";
51
51
  const req = await api.get("/", { headers: { authorization } });
52
52
  expect(req).not.toSucceed("Invalid Authorization type, 'Basic'");
53
53
  });
@@ -59,7 +59,7 @@ describe("token", () => {
59
59
  });
60
60
 
61
61
  it("cannot get the session", async () => {
62
- const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
62
+ const authorization = "Bearer REqA2l022l8Q0tuI";
63
63
  const req = await api.get("/", { headers: { authorization } });
64
64
  expect(req).not.toSucceed("Invalid session");
65
65
  });
@@ -69,29 +69,29 @@ describe("token", () => {
69
69
  id: ID,
70
70
  type: "token",
71
71
  provider: "email",
72
- user: "QypOn5SQApyOPdUpIZsO9u2O",
72
+ user: "QypOn5SQApyOPdUp",
73
73
  email: "abc@test.com",
74
74
  time: "2024-07-01T03:21:40Z",
75
75
  });
76
- const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
76
+ const authorization = "Bearer REqA2l022l8Q0tuI";
77
77
  const req = await api.get("/", { headers: { authorization } });
78
78
  expect(req).not.toSucceed("Credentials do not correspond to a user");
79
79
  });
80
80
 
81
81
  it("cannot get the user", async () => {
82
- store.set("auth:REqA2l022l8Q0tuIRtqLOPUy", {
83
- id: "REqA2l022l8Q0tuIRtqLOPUy",
82
+ store.set("auth:REqA2l022l8Q0tuI", {
83
+ id: "REqA2l022l8Q0tuI",
84
84
  type: "token",
85
85
  provider: "email",
86
- user: "QypOn5SQApyOPdUpIZsO9u2O",
86
+ user: "QypOn5SQApyOPdUp",
87
87
  email: "abc@test.com",
88
88
  time: "2024-07-01T03:21:40Z",
89
89
  });
90
- store.set("user:abc@test.com", {
91
- id: "QypOn5SQApyOPdUpIZsO9u2O",
90
+ store.set("user:QypOn5SQApyOPdUp", {
91
+ id: "QypOn5SQApyOPdUp",
92
92
  email: "abc@test.com",
93
93
  });
94
- const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
94
+ const authorization = "Bearer REqA2l022l8Q0tuI";
95
95
  const req = await api.get("/", { headers: { authorization } });
96
96
  expect(req).toSucceed();
97
97
  });
@@ -104,19 +104,19 @@ describe("cookie", () => {
104
104
  .test();
105
105
 
106
106
  it("should have the proper token in email", async () => {
107
- const cookie = "authorization=hello";
107
+ const cookie = "authentication=hello";
108
108
  const req = await api.get("/", { headers: { cookie } });
109
109
  expect(req).not.toSucceed("Invalid Authorization cookie");
110
110
  });
111
111
 
112
112
  it("can get the proper session", async () => {
113
- const cookie = "authorization=REqA2l022l8Q0tuIRtqLOPUy";
113
+ const cookie = "authentication=REqA2l022l8Q0tuI";
114
114
  const req = await api.get("/", { headers: { cookie } });
115
115
  expect(req).not.toSucceed("Invalid session");
116
116
  });
117
117
 
118
118
  it("can get the proper session", async () => {
119
- const cookie = "authorization=REqA2l022l8Q0tuIRtqLOPUy";
119
+ const cookie = "authentication=REqA2l022l8Q0tuI";
120
120
  const req = await api.get("/", { headers: { cookie } });
121
121
  expect(req).not.toSucceed();
122
122
  });
@@ -1,6 +1,5 @@
1
1
  import { createId } from "../../helpers/index.js";
2
2
  import { ServerError, status } from "../../index.js";
3
- import findUser from "../findUser.js";
4
3
  import updateUser from "../updateUser.js";
5
4
 
6
5
  const hash = new Proxy(
@@ -12,7 +11,7 @@ const hash = new Proxy(
12
11
  self,
13
12
  await import("argon2").catch(() => {
14
13
  throw new ServerError.AUTH_ARGON_NEEDED();
15
- })
14
+ }),
16
15
  );
17
16
  if (key === "verify" && !self.verify) {
18
17
  return async (hash, pass) => {
@@ -28,29 +27,29 @@ const hash = new Proxy(
28
27
  }
29
28
  return self[key];
30
29
  },
31
- }
30
+ },
32
31
  );
33
32
 
34
33
  const createSession = async (user, ctx) => {
35
- const { type, session, cleanUser } = ctx.options.auth;
34
+ const { type, session, cleanUser, redirect = "/user" } = ctx.options.auth;
36
35
  user = cleanUser(user);
37
- const token = createId();
36
+ const id = createId();
38
37
  const provider = "email";
39
38
  const time = new Date().toISOString().replace(/\.[0-9]*/, "");
40
39
  ctx.auth = {
41
- id: token,
40
+ id,
42
41
  type,
43
42
  provider,
44
- user: user.id,
43
+ user: user.email,
45
44
  email: user.email,
46
45
  time,
47
46
  };
48
- await session.set(token, ctx.auth, { expires: "1w" });
47
+ await session.set(id, ctx.auth, { expires: "1w" });
49
48
 
50
49
  if (type === "token") {
51
- return status(201).json({ ...user, token });
50
+ return status(201).json({ ...user, token: id });
52
51
  } else if (type === "cookie") {
53
- return status(201).cookies({ authorization: token }).send(user);
52
+ return status(302).cookies({ authentication: id }).redirect(redirect);
54
53
  } else if (type === "jwt") {
55
54
  throw new Error("JWT auth not supported yet");
56
55
  } else if (type === "key") {
@@ -87,10 +86,12 @@ async function register(ctx) {
87
86
  const store = ctx.options.auth.store;
88
87
  if (await store.has(email)) throw ServerError.REGISTER_EMAIL_EXISTS();
89
88
 
89
+ const time = new Date().toISOString().replace(/\.[0-9]*/, "");
90
90
  const user = {
91
- id: createId(),
91
+ id: createId(user.email),
92
92
  email,
93
93
  password: await hash.hash(password),
94
+ time,
94
95
  ...data,
95
96
  };
96
97
  await store.set(email, user);
@@ -123,7 +124,7 @@ async function reset(ctx) {
123
124
  async function password(ctx) {
124
125
  const { previous, updated } = ctx.body;
125
126
 
126
- const fullUser = await findUser(ctx.auth, ctx.options.auth.store);
127
+ const fullUser = await ctx.options.auth.store.get(ctx.auth.user);
127
128
 
128
129
  const isValid = await hash.verify(fullUser.password, previous);
129
130
  if (!isValid) throw ServerError.LOGIN_WRONG_PASSWORD();
@@ -0,0 +1,82 @@
1
+ import createId from "../../helpers/createId.js";
2
+ import { redirect, status } from "../../reply.js";
3
+
4
+ const oauth = async (code) => {
5
+ const fch = async (url, { body, headers = {}, ...rest } = {}) => {
6
+ headers.accept = "application/json";
7
+ headers["content-type"] = "application/json";
8
+ const res = await fetch(url, { ...rest, body, headers });
9
+ if (!res.ok) throw new Error("Invalid request");
10
+ return res.json();
11
+ };
12
+
13
+ const res = await fch("https://github.com/login/oauth/access_token", {
14
+ method: "post",
15
+ body: JSON.stringify({
16
+ client_id: env.GITHUB_ID,
17
+ client_secret: env.GITHUB_SECRET,
18
+ code,
19
+ }),
20
+ });
21
+ return (path) => {
22
+ return fch("https://api.github.com" + path, {
23
+ headers: { Authorization: "Bearer " + res.access_token },
24
+ });
25
+ };
26
+ };
27
+
28
+ const login = (ctx) => {
29
+ return redirect(
30
+ `https://github.com/login/oauth/authorize?client_id=${env.GITHUB_ID}&scope=user:email`,
31
+ );
32
+ };
33
+
34
+ const getUserProfile = async (code) => {
35
+ const api = await oauth(code);
36
+ const [profile, emails] = await Promise.all([
37
+ api("/user"),
38
+ api("/user/emails"),
39
+ ]);
40
+ const email = emails.sort((a) => (a.primary ? -1 : 1))[0]?.email;
41
+ return { ...profile, email };
42
+ };
43
+
44
+ const callback = async (ctx) => {
45
+ const { type, cleanUser, store, session, redirect } = ctx.options.auth;
46
+
47
+ const profile = await getUserProfile(ctx.url.query.code);
48
+
49
+ const auth = {
50
+ id: createId(),
51
+ type,
52
+ provider: "github",
53
+ user: createId(profile.email),
54
+ email: profile.email,
55
+ time: new Date().toISOString().replace(/\.[0-9]*/, ""),
56
+ };
57
+ const user = cleanUser({
58
+ id: profile.id,
59
+ name: profile.name,
60
+ email: profile.email,
61
+ picture: profile.avatar_url,
62
+ location: profile.location,
63
+ created: profile.created_at,
64
+ });
65
+
66
+ await store.set(auth.user, user);
67
+ await session.set(auth.id, auth, { expires: "1w" });
68
+
69
+ if (auth.type === "token") {
70
+ return status(201).json({ ...user, token: auth.id });
71
+ } else if (auth.type === "cookie") {
72
+ return status(302).cookies({ authentication: auth.id }).redirect(redirect);
73
+ } else if (auth.type === "jwt") {
74
+ throw new Error("JWT auth not supported yet");
75
+ } else if (auth.type === "key") {
76
+ throw new Error("Key auth not supported yet");
77
+ } else {
78
+ throw new Error("Unknown auth type");
79
+ }
80
+ };
81
+
82
+ export default { login, callback };
@@ -1,3 +1,4 @@
1
1
  import { default as email } from "./email.js";
2
+ import { default as github } from "./github.js";
2
3
 
3
- export default { email };
4
+ export default { email, github };
package/src/auth/user.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import ServerError from "../ServerError.js";
2
- import findUser from "./findUser.js";
3
2
 
4
3
  export default async function user(ctx) {
5
4
  if (!ctx.auth) return;
6
5
 
7
- const user = await findUser(ctx.auth, ctx.options.auth.store);
6
+ const user = await ctx.options.auth.store.get(ctx.auth.user);
8
7
  if (!user) throw ServerError.AUTH_NO_USER();
9
8
  return ctx.options.auth.cleanUser(user);
10
9
  }
@@ -41,15 +41,15 @@ describe("parseBody", () => {
41
41
  const body = await parseBody(
42
42
  getBody(),
43
43
  "multipart/form-data; boundary=----WebKitFormBoundaryvef1fLxmoUdYZWXp",
44
- { write: (id) => id }
44
+ { write: (id) => id },
45
45
  );
46
46
  expect(body).toMatchObject({
47
47
  hello: "world",
48
48
  test: ["test message 123456", "test message number two"],
49
49
  });
50
50
 
51
- const matchMd = expect.stringMatching(/^\w{24}.md$/);
52
- const matchTxt = expect.stringMatching(/^\w{24}.txt$/);
51
+ const matchMd = expect.stringMatching(/^\w{16}.md$/);
52
+ const matchTxt = expect.stringMatching(/^\w{16}.txt$/);
53
53
  expect(body).toMatchObject({
54
54
  profile: matchMd,
55
55
  gallery: [matchTxt, matchTxt],
@@ -58,10 +58,13 @@ export default function config(options) {
58
58
  }
59
59
  if (!options.auth.cleanUser) {
60
60
  options.auth.cleanUser = (fullUser) => {
61
- const { password, ...user } = fullUser;
61
+ const { password, token, ...user } = fullUser;
62
62
  return user;
63
63
  };
64
64
  }
65
+ if (!options.auth.redirect) {
66
+ options.auth.redirect = "/user";
67
+ }
65
68
  }
66
69
 
67
70
  return options;
@@ -12,12 +12,12 @@ describe("set-cookie", () => {
12
12
  it("sets the right cookie", async () => {
13
13
  const api = app.test();
14
14
  const res = await api.get("/hello");
15
- expect(res.headers["set-cookie"]).toBe("hello=world");
15
+ expect(res.headers["set-cookie"]).toBe("hello=world;Path=/");
16
16
  });
17
17
 
18
18
  it("can set multiple cookies", async () => {
19
19
  const api = app.test();
20
20
  const res = await api.get("/multiple");
21
- expect(res.headers["set-cookie"]).toEqual(["a=b", "c=d"]);
21
+ expect(res.headers["set-cookie"]).toEqual(["a=b;Path=/", "c=d;Path=/"]);
22
22
  });
23
23
  });
@@ -9,7 +9,7 @@ export default function createCookies(cookies) {
9
9
  val = { value: val };
10
10
  }
11
11
  const { value, path, expires } = val;
12
- const pathPart = path ? ";Path=" + path : "";
12
+ const pathPart = ";Path=" + (path || "/");
13
13
  const expiresPart = expires ? ";Expires=" + expires : "";
14
14
  return `${key}=${value || ""}${pathPart}${expiresPart}`;
15
15
  });
@@ -1,17 +1,52 @@
1
- const urlAlphabet =
1
+ const alphabet =
2
2
  "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
3
3
 
4
4
  export let random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
5
5
 
6
- export default function createId() {
7
- let size = 24;
6
+ // Credit: https://stackoverflow.com/a/52171480/938236
7
+ const cyrb53 = (str, seed = 0) => {
8
+ if (typeof str !== "string") str = String(str);
9
+ let h1 = 0xdeadbeef ^ seed;
10
+ let h2 = 0x41c6ce57 ^ seed;
11
+ for (let i = 0, ch; i < str.length; i++) {
12
+ ch = str.charCodeAt(i);
13
+ h1 = Math.imul(h1 ^ ch, 2654435761);
14
+ h2 = Math.imul(h2 ^ ch, 1597334677);
15
+ }
16
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
17
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
18
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
19
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
20
+
21
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
22
+ };
23
+
24
+ const hash = (str, size) => {
25
+ let chars = "";
26
+
27
+ let num = cyrb53(str);
28
+ for (let i = 0; i < size; i++) {
29
+ if (num < alphabet.length) num = cyrb53(str, i);
30
+ chars += alphabet[num % alphabet.length];
31
+ num = Math.floor(num / alphabet.length);
32
+ }
33
+ return chars;
34
+ };
35
+
36
+ const randomId = (size = 16) => {
8
37
  let id = "";
9
- let bytes = crypto.getRandomValues(new Uint8Array(size));
38
+ let bytes = random(size);
10
39
  while (size--) {
11
40
  // Using the bitwise AND operator to "cap" the value of
12
41
  // the random byte from 255 to 63, in that way we can make sure
13
42
  // that the value will be a valid index for the "chars" string.
14
- id += urlAlphabet[bytes[size] & 61];
43
+ id += alphabet[bytes[size] & 61];
15
44
  }
16
45
  return id;
46
+ };
47
+
48
+ export default function createId(source) {
49
+ const size = 16;
50
+ if (source) return hash(source, size);
51
+ return randomId(size);
17
52
  }
@@ -1,21 +1,9 @@
1
- import middle from "../middle/index.js";
2
1
  import parseResponse from "../parseResponse.js";
3
2
  import pathPattern from "../pathPattern.js";
4
3
  import define from "./define.js";
5
4
  import validate from "./validate.js";
6
5
 
7
- const extendWithDefaults = (ctx) => {
8
- // TODO: find a better way of doing this FFS
9
- // Only want to execute it once; it needs to happen on a per-request
10
- // basis since we only have full access to the options there
11
- if (ctx.app.extended) return;
12
- middle(ctx);
13
- ctx.app.extended = true;
14
- };
15
-
16
6
  export default async function handleRequest(handlers, ctx) {
17
- extendWithDefaults(ctx);
18
-
19
7
  for (let [method, matcher, ...cbs] of handlers[ctx.method]) {
20
8
  const match = pathPattern(matcher, ctx.url.pathname || "/");
21
9
  // Skip this whole middleware if there was no match
package/src/index.js CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  parseHeaders,
12
12
  } from "./helpers/index.js";
13
13
 
14
+ import middle from "./middle/index.js";
15
+
14
16
  // Export the reply helpers
15
17
  export * from "./reply.js";
16
18
 
@@ -71,6 +73,8 @@ export default function server(options = {}) {
71
73
  if (this.platform.runtime === "node") {
72
74
  this.node();
73
75
  }
76
+
77
+ this.use(...middle);
74
78
  }
75
79
 
76
80
  server.prototype.self = function () {
@@ -146,6 +150,8 @@ server.prototype.fetch = async function (request, env) {
146
150
  // #region HTTP methods
147
151
  // INTERNAL
148
152
  server.prototype.handle = function (method, path, ...middleware) {
153
+ // Do not try to optimize, we NEED the method to remain '*' here so that
154
+ // it doesn't auto-finish
149
155
  if (method === "*") {
150
156
  for (let m in this.handlers) {
151
157
  this.handlers[m].push([method, path, ...middleware]);
@@ -224,17 +230,17 @@ server.prototype.test = function () {
224
230
  );
225
231
 
226
232
  const headers = parseHeaders(res.headers);
227
- let data;
233
+ let body;
228
234
  if (headers["set-cookie"]) {
229
235
  // TODO: this should really be a smart merge of the 2
230
236
  cookie = headers["set-cookie"];
231
237
  }
232
238
  if (headers["content-type"]?.includes("application/json")) {
233
- data = await res.json();
239
+ body = await res.json();
234
240
  } else {
235
- data = await res.text();
241
+ body = await res.text();
236
242
  }
237
- return { status: res.status, headers, data };
243
+ return { status: res.status, headers, body, data: body };
238
244
  };
239
245
  return {
240
246
  app: this,
@@ -1,6 +1,8 @@
1
1
  import { type } from "../reply.js";
2
2
 
3
3
  export default async function assets(ctx) {
4
+ if (!ctx.options.public) return;
5
+ if (!ctx.method !== "get") return;
4
6
  try {
5
7
  // TODO: streaming
6
8
  // Read it as buffer (null)
@@ -1,12 +1,4 @@
1
1
  import auth from "../auth/index.js";
2
2
  import assets from "./assets.js";
3
3
 
4
- export default async function middle(ctx) {
5
- // Serve assets
6
- if (ctx.options.public) {
7
- // We need these before other endpoints
8
- ctx.app.handlers.get.unshift(["*", "*", assets]);
9
- }
10
-
11
- await auth.middle(ctx);
12
- }
4
+ export default [assets, auth.middle];
@@ -19,7 +19,7 @@ export default async function parseResponse(out, ctx) {
19
19
 
20
20
  // A plain string will be converted to either html or plain
21
21
  if (typeof out === "string") {
22
- const type = /\s*\</.test(out) ? "text/html" : "text/plain";
22
+ const type = /^\s*\</.test(out) ? "text/html" : "text/plain";
23
23
  out = new Response(out, { headers: { "content-type": type } });
24
24
  }
25
25
 
package/src/reply.js CHANGED
@@ -60,6 +60,10 @@ Reply.prototype.json = function (body) {
60
60
  }).send(JSON.stringify(body));
61
61
  };
62
62
 
63
+ Reply.prototype.redirect = function (Location) {
64
+ return this.headers({ Location }).status(302).send();
65
+ };
66
+
63
67
  Reply.prototype.file = async function (path) {
64
68
  try {
65
69
  const data = await fs.readFile(path);
@@ -133,4 +137,5 @@ export const cookies = (...args) => new Reply().cookies(...args);
133
137
  export const send = (...args) => new Reply().send(...args);
134
138
  export const json = (...args) => new Reply().json(...args);
135
139
  export const file = (...args) => new Reply().file(...args);
140
+ export const redirect = (...args) => new Reply().redirect(...args);
136
141
  export const view = (...args) => new Reply().view(...args);
@@ -15,7 +15,7 @@ describe("can route properly", () => {
15
15
  // INTERNAL - so this might change in the future
16
16
  it("has the correct structure", () => {
17
17
  const registeredPaths = app.handlers.get.map((h) => h[1]);
18
- expect(registeredPaths).toEqual(["/hello", "/api/hello", "/"]);
18
+ expect(registeredPaths).toEqual(["*", "/hello", "/api/hello", "/"]);
19
19
  });
20
20
 
21
21
  it("can get fallback when nothing matches", async () => {
@@ -1,5 +0,0 @@
1
- export default async function findUser(auth, store) {
2
- if (auth.provider === "email") {
3
- return await store.get(auth.email);
4
- }
5
- }