@server/next 0.20.7 → 0.20.8

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.7",
3
+ "version": "0.20.8",
4
4
  "description": "An experimental reimplementation of server.js focused on the DX",
5
5
  "homepage": "https://node-server.com/",
6
6
  "repository": "https://github.com/franciscop/server-next.git",
@@ -9,7 +9,7 @@
9
9
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
10
10
  "license": "UNLICENSED",
11
11
  "scripts": {
12
- "demo": "nodemon ./demo/app.js",
12
+ "demo": "nodemon ./demo/src/index.js",
13
13
  "start": "bun test --watch",
14
14
  "test": "bun test",
15
15
  "test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
package/src/auth.test.js CHANGED
@@ -28,42 +28,51 @@ describe("auth", () => {
28
28
  expect(await res.text()).toBe("Bearer REqA2l022l8Q0tuIRtqLOPUy");
29
29
  });
30
30
 
31
- describe.skip("user creation flow", () => {
32
- const url = (path, options = {}) =>
31
+ describe("user creation flow", () => {
32
+ // These are obviously mock data
33
+ const EMAIL = "abc@test.com";
34
+ const PASS = "11111111";
35
+
36
+ const url = (path, body = {}, headers = {}) =>
33
37
  new Request("http://localhost:3000" + path, {
34
38
  headers: {
35
39
  cookie: "session=REqA2l022l8Q0tuIRtqLOPUy",
36
40
  "content-type": "application/json",
41
+ ...headers,
37
42
  },
38
- ...options,
39
- body: JSON.stringify(options.body),
43
+ method: "POST",
44
+ body: JSON.stringify(body),
40
45
  });
41
46
 
42
47
  const store = kv(new Map());
43
- const app = server({ auth: "email", store });
48
+ const app = server({ auth: { type: "token", providers: "email" }, store });
44
49
 
45
50
  it("can create a new user", async () => {
46
- const registered = await app.fetch(
47
- url("/register", {
48
- method: "POST",
49
- body: { email: "abc@test.com", password: "11111111" },
50
- })
51
+ const regReq = await app.fetch(
52
+ url("/auth/register/email", { email: EMAIL, password: PASS })
53
+ );
54
+ const register = await regReq.json();
55
+ expect(regReq.status).toBe(201);
56
+ expect(await store.keys()).toEqual([
57
+ "auth:abc@test.com",
58
+ "session:" + register.token,
59
+ ]);
60
+
61
+ const logoutReq = await app.fetch(
62
+ url("/auth/logout", {}, { authorization: "Bearer " + register.token })
51
63
  );
52
- console.log(registered);
53
- console.log(Object.fromEntries(await store.entries()));
54
- console.log(await registered.text());
55
- expect(registered.status).toBe(201);
64
+ expect(logoutReq.status).toBe(200);
65
+ expect(await store.keys()).toEqual(["auth:abc@test.com"]);
56
66
 
57
- const login = await app.fetch(
58
- url("/login", {
59
- method: "POST",
60
- body: { email: "abc@test.com", password: "11111111" },
61
- })
67
+ const loginReq = await app.fetch(
68
+ url("/auth/login/email", { email: EMAIL, password: PASS })
62
69
  );
63
- console.log(login);
64
- console.log(Object.fromEntries(await store.entries()));
65
- console.log(await login.text());
66
- expect(login.status).toBe(200);
70
+ const login = await loginReq.json();
71
+ expect(loginReq.status).toBe(200);
72
+ expect(await store.keys()).toEqual([
73
+ "auth:abc@test.com",
74
+ "session:" + login.token,
75
+ ]);
67
76
  });
68
77
  });
69
78
  });
@@ -1,4 +1,4 @@
1
- import { ServerError } from "../";
1
+ import { ServerError } from "../index.js";
2
2
 
3
3
  export default async function findAuth(ctx) {
4
4
  // NO AUTH AT ALL
@@ -7,7 +7,11 @@ export default async function findAuth(ctx) {
7
7
  if (!store) return;
8
8
 
9
9
  // AUTHENTICATION IS AVAILABLE
10
- ctx.auth = { type: ctx.options.auth.type, store };
10
+ ctx.auth = {
11
+ type: ctx.options.auth.type,
12
+ providers: ctx.options.auth.providers,
13
+ store,
14
+ };
11
15
 
12
16
  // If the user is not authenticated, there's no auth to retrieve
13
17
  if (!ctx.headers.authorization) return;
@@ -1,4 +1,4 @@
1
- import { ServerError } from "../";
1
+ import { ServerError } from "../index.js";
2
2
 
3
3
  class NoSession {}
4
4
 
@@ -1,10 +1,18 @@
1
- import ServerError from "../ServerError";
1
+ import ServerError from "../ServerError.js";
2
2
 
3
3
  ServerError.extend({
4
+ NO_STORE: `You need a 'store' to write 'ctx.session'`,
4
5
  NO_STORE_WRITE: `You need a 'store' to write 'ctx.session.{key}'`,
5
6
  NO_STORE_READ: `You need a 'store' to read 'ctx.session.{key}'`,
6
7
  AUTH_INVALID_TYPE: `Invalid Authorization type, '{type}'`,
7
8
  AUTH_INVALID_TOKEN: `Invalid Authorization token`,
9
+
10
+ LOGIN_NO_EMAIL: "The email is required to log in",
11
+ LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
12
+ LOGIN_NO_PASSWORD: "The email is required to log in",
13
+ LOGIN_INVALID_PASSWORD: "The email you wrote is not correct",
14
+ LOGIN_WRONG_ACCOUNT: `That email does not correspond to any account`,
15
+ LOGIN_WRONG_PASSWORD: `That is not the valid password`,
8
16
  });
9
17
 
10
18
  export default ServerError;
@@ -1,40 +1,71 @@
1
1
  import argon2 from "argon2";
2
2
 
3
- import { createId } from "../helpers";
4
- import { status } from "../reply";
3
+ import { createId } from "../helpers/index.js";
4
+ import { status, type } from "../reply.js";
5
+ import ServerError from "../ServerError.js";
5
6
 
6
7
  export default function middle(ctx) {
7
- // if (ctx.auth?.type?.includes("email")) {
8
- // ctx.app.post("/register", async (ctx) => {
9
- // const { email, password, ...data } = ctx.body;
10
- // if (!email || !/@/.test(email)) throw new Error("Email needed");
11
- // if (!password || password.length < 8) throw new Error("Password needed");
12
- // const id = createId();
13
- // const time = new Date().toISOString();
14
- // const pass = await argon2.hash(password);
15
- // await ctx.auth.store.set(email, { id, email, password: pass, ...data });
16
- // const token = await ctx.options.session.store.add({ id, email, time });
17
- // return status(201).send({ id, token, email, ...data });
18
- // });
19
- //
20
- // ctx.app.post("/login", async (ctx) => {
21
- // const { email, password } = ctx.body;
22
- // if (!email || !/@/.test(email)) throw new Error("Email needed");
23
- // if (!password || password.length < 8) throw new Error("Password needed");
24
- // const time = new Date().toISOString();
25
- // const user = await ctx.auth.store.get(email);
26
- // if (!user) {
27
- // return status(400).send("Invalid email");
28
- // }
29
- // if (!(await argon2.verify(user.password, password))) {
30
- // return status(400).send("Invalid password");
31
- // }
32
- // const token = await ctx.options.session.store.add({
33
- // id: user.id,
34
- // email,
35
- // time,
36
- // });
37
- // return { id: user.id, token, email };
38
- // });
39
- // }
8
+ if (ctx.options.public) {
9
+ ctx.app.handlers.get.unshift([
10
+ "*",
11
+ "*",
12
+ async function publicFolder(ctx) {
13
+ try {
14
+ const asset = await ctx.options.public.read(ctx.url.pathname);
15
+ if (asset) {
16
+ return type(ctx.url.pathname.split(".").pop()).send(asset);
17
+ }
18
+ } catch (error) {}
19
+ },
20
+ ]);
21
+ }
22
+
23
+ if (!ctx.auth) return;
24
+ if (ctx.auth?.providers?.includes("email")) {
25
+ ctx.app.post("/auth/register/email", async (ctx) => {
26
+ const { email, password, ...data } = ctx.body;
27
+ if (!email || !/@/.test(email)) throw new Error("Email needed");
28
+ if (!password || password.length < 8) throw new Error("Password needed");
29
+ if (await ctx.auth.store.has(email)) {
30
+ throw new Error("Email is already registered");
31
+ }
32
+ const id = createId();
33
+ const time = new Date().toISOString();
34
+ const pass = await argon2.hash(password);
35
+ await ctx.auth.store.set(email, { id, email, password: pass, ...data });
36
+
37
+ // TYPE GOES HERE
38
+ const token = await ctx.options.session.store.add({ id, email, time });
39
+ return status(201).json({ id, token, email, ...data });
40
+ });
41
+
42
+ ctx.app.post("/auth/logout", async (ctx) => {
43
+ if (ctx.auth.id) {
44
+ await ctx.options.session.store.del(ctx.auth.id);
45
+ }
46
+ return status(200).send();
47
+ });
48
+
49
+ ctx.app.post("/auth/login/email", async (ctx) => {
50
+ const { email, password } = ctx.body;
51
+ if (!email) throw ServerError.LOGIN_NO_EMAIL();
52
+ if (!/@/.test(email)) throw ServerError.LOGIN_INVALID_EMAIL();
53
+ if (!password) throw ServerError.LOGIN_NO_PASSWORD();
54
+ if (password.length < 8) throw ServerError.LOGIN_INVALID_PASSWORD();
55
+
56
+ const time = new Date().toISOString();
57
+ const user = await ctx.auth.store.get(email);
58
+ if (!user) throw ServerError.LOGIN_WRONG_EMAIL();
59
+ const isValid = await argon2.verify(user.password, password);
60
+ if (!isValid) throw ServerError.LOGIN_WRONG_PASSWORD();
61
+
62
+ // TYPE GOES HERE
63
+ const token = await ctx.options.session.store.add({
64
+ id: user.id,
65
+ email,
66
+ time,
67
+ });
68
+ return { id: user.id, token, email };
69
+ });
70
+ }
40
71
  }
@@ -42,21 +42,16 @@ export default async function parseResponse(out, ctx) {
42
42
  // If we have a session, we need to persist it into a cookie
43
43
  if (Object.keys(ctx.session || {}).length) {
44
44
  if (!ctx.options.session?.store) {
45
- throw ServerError.NO_STORE_WRITE({});
45
+ throw ServerError.NO_STORE({});
46
46
  }
47
47
 
48
- let id;
49
48
  // Persistence is based on the Token
50
- if (ctx.auth) {
51
- id = ctx.auth.id;
52
- } else {
53
- // Persistence is based on the Cookies
54
- // No session cookies, generate a _persistent_ cookie
55
- if (!ctx.cookies.session) {
56
- ctx.res.cookies.session = createId();
57
- }
58
- id = ctx.cookies.session;
49
+ // Persistence is based on the Cookies
50
+ // No session cookies, generate a _persistent_ cookie
51
+ if (!ctx.cookies.session) {
52
+ ctx.res.cookies.session = createId();
59
53
  }
54
+ const id = ctx.cookies.session;
60
55
 
61
56
  // Saves the session in the session store
62
57
  // Note that this is async but we are totally fine deferring it
package/src/reply.js CHANGED
@@ -2,9 +2,12 @@ import fs from "fs/promises";
2
2
 
3
3
  import { createCookies, types } from "./helpers/index.js";
4
4
 
5
- function Reply() {}
6
-
7
- Reply.prototype.res = { headers: {}, cookies: {} };
5
+ function Reply() {
6
+ this.res = {
7
+ headers: {},
8
+ cookies: {},
9
+ };
10
+ }
8
11
 
9
12
  // INTERNAL
10
13
  Reply.prototype.generateHeaders = function () {
@@ -49,7 +52,7 @@ Reply.prototype.cookies = function (cookies) {
49
52
 
50
53
  // FINAL
51
54
  Reply.prototype.json = function (body) {
52
- return headers({ "content-type": "application/json" }).send(
55
+ return this.headers({ "content-type": "application/json" }).send(
53
56
  JSON.stringify(body)
54
57
  );
55
58
  };
@@ -74,7 +77,7 @@ Reply.prototype.view = async function (path) {
74
77
  }
75
78
  const data = await ctx.options.views.read(path);
76
79
  if (data) return this.type(path.split(".").pop()).send(data);
77
- return status(404).send();
80
+ return this.status(404).send();
78
81
  };
79
82
  };
80
83
 
@@ -1,4 +1,4 @@
1
- import server, { router } from "./index.js";
1
+ import server, { router, status } from "./index.js";
2
2
 
3
3
  const url = (path, options = {}) =>
4
4
  new Request("http://localhost:3000" + path, options);
@@ -44,4 +44,15 @@ describe("can route properly", () => {
44
44
  const res = await app.fetch(url("/api/hello", { method: "POST" }));
45
45
  expect(await res.text()).toBe("Hello /api/hello");
46
46
  });
47
+
48
+ it("no status reuse", async () => {
49
+ const app = server()
50
+ .get("/a", () => status(201).send("hello"))
51
+ .get("/b", () => ({ hello: "bye" }))
52
+ .get("/", () => "Fallback");
53
+ const resA = await app.fetch(url("/a"));
54
+ expect(resA.status).toBe(201);
55
+ const resB = await app.fetch(url("/b"));
56
+ expect(resB.status).toBe(200);
57
+ });
47
58
  });