@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 +2 -2
- package/src/auth.test.js +32 -23
- package/src/context/findAuth.js +6 -2
- package/src/context/findSession.js +1 -1
- package/src/errors/index.js +9 -1
- package/src/middle/index.js +66 -35
- package/src/parseResponse.js +6 -11
- package/src/reply.js +8 -5
- package/src/router.test.js +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.20.
|
|
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/
|
|
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
|
|
32
|
-
|
|
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
|
-
|
|
39
|
-
body: JSON.stringify(
|
|
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
|
|
47
|
-
url("/register", {
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
});
|
package/src/context/findAuth.js
CHANGED
|
@@ -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 = {
|
|
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;
|
package/src/errors/index.js
CHANGED
|
@@ -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;
|
package/src/middle/index.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
}
|
package/src/parseResponse.js
CHANGED
|
@@ -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.
|
|
45
|
+
throw ServerError.NO_STORE({});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
let id;
|
|
49
48
|
// Persistence is based on the Token
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
package/src/router.test.js
CHANGED
|
@@ -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
|
});
|