@server/next 0.20.8 → 0.20.10
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/index.d.ts +1 -0
- package/package.json +1 -2
- package/readme.md +5 -5
- package/src/auth/NoSession.js +18 -0
- package/src/auth/auth-cookie.test.js +41 -0
- package/src/auth/auth-token.test.js +42 -0
- package/src/auth/auth.js +52 -0
- package/src/auth/index.js +24 -0
- package/src/auth/index.test.js +123 -0
- package/src/auth/logout.js +18 -0
- package/src/auth/providers/email.js +73 -0
- package/src/auth/providers/index.js +3 -0
- package/src/auth/session.js +18 -0
- package/src/auth/user.js +21 -0
- package/src/context/node.js +9 -5
- package/src/context/winter.js +4 -6
- package/src/errors/index.js +14 -0
- package/src/helpers/cookies.test.js +23 -0
- package/src/helpers/createCookies.js +13 -10
- package/src/helpers/index.js +1 -0
- package/src/helpers/parseHeaders.js +15 -0
- package/src/index.js +68 -3
- package/src/index.test.js +24 -30
- package/src/middle/assets.js +12 -0
- package/src/middle/index.js +7 -66
- package/src/parseResponse.js +3 -1
- package/src/reply.js +5 -2
- package/src/router.test.js +21 -22
- package/src/session.test.js +26 -26
- package/src/test/toSucceed.js +64 -0
- package/src/url.test.js +13 -11
- package/src/auth.test.js +0 -78
- package/src/context/findAuth.js +0 -32
- package/src/context/findSession.js +0 -34
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.10",
|
|
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,6 @@
|
|
|
9
9
|
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"demo": "nodemon ./demo/src/index.js",
|
|
13
12
|
"start": "bun test --watch",
|
|
14
13
|
"test": "bun test",
|
|
15
14
|
"test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
package/readme.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
> **⚠️ WIP** This is an **experimental library** right now!
|
|
4
4
|
|
|
5
|
-
[**Documentation Here**](https://node-server.com/documentation/)
|
|
6
|
-
|
|
7
5
|
A fully-fledged web server for Bun and Node.js, with all the basics covered for you:
|
|
8
6
|
|
|
9
7
|
```js
|
|
@@ -26,11 +24,11 @@ It includes all the things you would expect from a modern Server framework, like
|
|
|
26
24
|
```js
|
|
27
25
|
// Easy testing as well - index.test.js
|
|
28
26
|
import app from "./";
|
|
27
|
+
const api = app.test(); // Very convenient helper, AXIOS-like interface
|
|
29
28
|
|
|
30
29
|
it("can retrieve the homepage", async () => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
expect(books[0]).toEqual({ id: 0, name: 'The Catcher In The Rye', ... });
|
|
30
|
+
const { data: books } = await api.get("/books/");
|
|
31
|
+
expect(books[0]).toEqual({ id: 0, name: ... });
|
|
34
32
|
});
|
|
35
33
|
```
|
|
36
34
|
|
|
@@ -56,6 +54,8 @@ Why? We live in the era of multi-cloud (Heroku, Workers, Lambda, etc) and multi-
|
|
|
56
54
|
- A readStream and it'll be piped to the response
|
|
57
55
|
- An object with `status`, `body` and `headers` and it'll be set raw.
|
|
58
56
|
- Response compression works
|
|
57
|
+
- Zod light integration
|
|
58
|
+
- Auth work
|
|
59
59
|
|
|
60
60
|
## Examples
|
|
61
61
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ServerError } from "../index.js";
|
|
2
|
+
|
|
3
|
+
class NoSession {}
|
|
4
|
+
|
|
5
|
+
export default new Proxy(NoSession, {
|
|
6
|
+
get(target, key) {
|
|
7
|
+
if (target[key]) return target[key];
|
|
8
|
+
if (key === "then") return target[key];
|
|
9
|
+
throw ServerError.NO_STORE_READ({ key });
|
|
10
|
+
},
|
|
11
|
+
set(target, key, value) {
|
|
12
|
+
if (target[key] || key === "then") {
|
|
13
|
+
target[key] = value;
|
|
14
|
+
} else {
|
|
15
|
+
throw ServerError.NO_STORE_WRITE({ key });
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import "../test/toSucceed.js";
|
|
2
|
+
|
|
3
|
+
import kv from "polystore";
|
|
4
|
+
|
|
5
|
+
import server from "../index.js";
|
|
6
|
+
|
|
7
|
+
describe("user creation flow", () => {
|
|
8
|
+
// These are obviously mock data
|
|
9
|
+
const EMAIL = "abc@test.com";
|
|
10
|
+
const PASS = "11111111";
|
|
11
|
+
const CREDENTIALS = { email: EMAIL, password: PASS };
|
|
12
|
+
|
|
13
|
+
const store = kv(new Map());
|
|
14
|
+
const api = server({ store, auth: "cookie:email" })
|
|
15
|
+
.get("/me", (ctx) => ctx.user || "No data")
|
|
16
|
+
.test();
|
|
17
|
+
|
|
18
|
+
it("can create a new user", async () => {
|
|
19
|
+
const register = await api.post("/auth/register/email", CREDENTIALS);
|
|
20
|
+
expect(register).toSucceed();
|
|
21
|
+
expect(await store.keys()).toEqual([
|
|
22
|
+
"user:abc@test.com",
|
|
23
|
+
"auth:" + register.headers["set-cookie"].split("=")[1],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const me = await api.get("/me");
|
|
27
|
+
expect(me).toSucceed();
|
|
28
|
+
expect(me.data.email).toEqual(EMAIL);
|
|
29
|
+
|
|
30
|
+
const logout = await api.post("/auth/logout");
|
|
31
|
+
expect(logout).toSucceed();
|
|
32
|
+
expect(await store.keys()).toEqual(["user:abc@test.com"]);
|
|
33
|
+
|
|
34
|
+
const login = await api.post("/auth/login/email", CREDENTIALS);
|
|
35
|
+
expect(login).toSucceed();
|
|
36
|
+
expect(await store.keys()).toEqual([
|
|
37
|
+
"user:abc@test.com",
|
|
38
|
+
"auth:" + login.headers["set-cookie"].split("=")[1],
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import "../test/toSucceed.js";
|
|
2
|
+
|
|
3
|
+
import kv from "polystore";
|
|
4
|
+
|
|
5
|
+
import server from "../index.js";
|
|
6
|
+
|
|
7
|
+
describe("user creation flow", () => {
|
|
8
|
+
// These are obviously mock data
|
|
9
|
+
const EMAIL = "abc@test.com";
|
|
10
|
+
const PASS = "11111111";
|
|
11
|
+
const CREDENTIALS = { email: EMAIL, password: PASS };
|
|
12
|
+
|
|
13
|
+
const store = kv(new Map());
|
|
14
|
+
const api = server({ store, auth: "token:email" })
|
|
15
|
+
.get("/me", (ctx) => ctx.user || "No data")
|
|
16
|
+
.test();
|
|
17
|
+
|
|
18
|
+
it("can create a new user", async () => {
|
|
19
|
+
const register = await api.post("/auth/register/email", CREDENTIALS);
|
|
20
|
+
expect(register).toSucceed();
|
|
21
|
+
expect(await store.keys()).toEqual([
|
|
22
|
+
"user:abc@test.com",
|
|
23
|
+
"auth:" + register.data.token,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const headers = { authorization: "Bearer " + register.data.token };
|
|
27
|
+
const me = await api.get("/me", { headers });
|
|
28
|
+
expect(me).toSucceed();
|
|
29
|
+
expect(me.data.email).toEqual(EMAIL);
|
|
30
|
+
|
|
31
|
+
const logout = await api.post("/auth/logout", {}, { headers });
|
|
32
|
+
expect(logout).toSucceed();
|
|
33
|
+
expect(await store.keys()).toEqual(["user:abc@test.com"]);
|
|
34
|
+
|
|
35
|
+
const login = await api.post("/auth/login/email", CREDENTIALS);
|
|
36
|
+
expect(login).toSucceed();
|
|
37
|
+
expect(await store.keys()).toEqual([
|
|
38
|
+
"user:abc@test.com",
|
|
39
|
+
"auth:" + login.data.token,
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/auth/auth.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ServerError } from "../index.js";
|
|
2
|
+
|
|
3
|
+
const validateToken = (authorization) => {
|
|
4
|
+
const [type, id] = authorization.trim().split(" ");
|
|
5
|
+
if (type.toLowerCase() !== "bearer") {
|
|
6
|
+
throw ServerError.AUTH_INVALID_TYPE({ type });
|
|
7
|
+
}
|
|
8
|
+
if (id.length !== 24) {
|
|
9
|
+
throw ServerError.AUTH_INVALID_TOKEN();
|
|
10
|
+
}
|
|
11
|
+
return id;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const validateCookie = (authorization) => {
|
|
15
|
+
if (authorization.length !== 24) {
|
|
16
|
+
throw ServerError.AUTH_INVALID_COOKIE();
|
|
17
|
+
}
|
|
18
|
+
return authorization;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const findSessionId = (ctx) => {
|
|
22
|
+
const type = ctx.options.auth.type;
|
|
23
|
+
|
|
24
|
+
if (type === "token") {
|
|
25
|
+
// If the user is not authenticated, there's no auth to retrieve
|
|
26
|
+
if (!ctx.headers.authorization) return;
|
|
27
|
+
|
|
28
|
+
// Check the authentication header
|
|
29
|
+
return validateToken(ctx.headers.authorization);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (type === "cookie") {
|
|
33
|
+
// If the user is not authenticated, there's no auth to retrieve
|
|
34
|
+
if (!ctx.cookies.authorization) return;
|
|
35
|
+
|
|
36
|
+
return validateCookie(ctx.cookies.authorization);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error("Invalid auth type " + type);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default async function findAuth(ctx) {
|
|
43
|
+
if (!ctx.options.auth) return; // NO AUTH AT ALL; nothing to do here
|
|
44
|
+
const options = ctx.options.auth;
|
|
45
|
+
|
|
46
|
+
const sessionId = findSessionId(ctx);
|
|
47
|
+
if (!sessionId) return; // NO SESSION FOUND; no auth
|
|
48
|
+
|
|
49
|
+
const auth = await options.session.get(sessionId);
|
|
50
|
+
if (!auth) throw ServerError.AUTH_NO_SESSION();
|
|
51
|
+
return auth;
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import auth from "./auth.js";
|
|
2
|
+
import logout from "./logout.js";
|
|
3
|
+
import providers from "./providers/index.js";
|
|
4
|
+
import session from "./session.js";
|
|
5
|
+
import user from "./user.js";
|
|
6
|
+
|
|
7
|
+
const load = async (ctx) => {
|
|
8
|
+
ctx.session = await session(ctx);
|
|
9
|
+
ctx.auth = await auth(ctx);
|
|
10
|
+
ctx.user = await user(ctx);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const middle = async (ctx) => {
|
|
14
|
+
if (ctx.options.auth) {
|
|
15
|
+
ctx.app.post("/auth/logout", logout);
|
|
16
|
+
|
|
17
|
+
if (ctx.options.auth.provider.includes("email")) {
|
|
18
|
+
ctx.app.post("/auth/register/email", providers.email.register);
|
|
19
|
+
ctx.app.post("/auth/login/email", providers.email.login);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default { load, middle };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import "../test/toSucceed.js";
|
|
2
|
+
|
|
3
|
+
import kv from "polystore";
|
|
4
|
+
|
|
5
|
+
import server from "../index.js";
|
|
6
|
+
|
|
7
|
+
const ID = "REqA2l022l8Q0tuIRtqLOPUy";
|
|
8
|
+
|
|
9
|
+
describe("auth", () => {
|
|
10
|
+
it("requires a provider", () => {
|
|
11
|
+
const store = kv(new Map());
|
|
12
|
+
const api = server({ store, auth: "token:email" })
|
|
13
|
+
.get("/", (ctx) => ctx.auth)
|
|
14
|
+
.test();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("provider must belong", async () => {
|
|
18
|
+
const store = kv(new Map());
|
|
19
|
+
const api = server({ store, auth: "token:email" })
|
|
20
|
+
.get("/", (ctx) => ctx.auth)
|
|
21
|
+
.test();
|
|
22
|
+
|
|
23
|
+
store.set(`auth:${ID}`, {
|
|
24
|
+
id: ID,
|
|
25
|
+
type: "token",
|
|
26
|
+
provider: "wrong",
|
|
27
|
+
user: "QypOn5SQApyOPdUpIZsO9u2O",
|
|
28
|
+
email: "abc@test.com",
|
|
29
|
+
time: "2024-07-01T03:21:40Z",
|
|
30
|
+
});
|
|
31
|
+
const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
|
|
32
|
+
const req = await api.get("/", { headers: { authorization } });
|
|
33
|
+
expect(req).not.toSucceed(
|
|
34
|
+
'Invalid provider "wrong", valid ones are: "email"'
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("token", () => {
|
|
40
|
+
const store = kv(new Map());
|
|
41
|
+
const api = server({ store, auth: "token:email" })
|
|
42
|
+
.get("/", (ctx) => ctx.auth)
|
|
43
|
+
.test();
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await store.del(`auth:${ID}`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should be Bearer", async () => {
|
|
50
|
+
const authorization = "Basic REqA2l022l8Q0tuIRtqLOPUy";
|
|
51
|
+
const req = await api.get("/", { headers: { authorization } });
|
|
52
|
+
expect(req).not.toSucceed("Invalid Authorization type, 'Basic'");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should have the proper token", async () => {
|
|
56
|
+
const authorization = "Bearer hola";
|
|
57
|
+
const req = await api.get("/", { headers: { authorization } });
|
|
58
|
+
expect(req).not.toSucceed("Invalid Authorization token");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("cannot get the session", async () => {
|
|
62
|
+
const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
|
|
63
|
+
const req = await api.get("/", { headers: { authorization } });
|
|
64
|
+
expect(req).not.toSucceed("Invalid session");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("cannot get the user", async () => {
|
|
68
|
+
store.set(`auth:${ID}`, {
|
|
69
|
+
id: ID,
|
|
70
|
+
type: "token",
|
|
71
|
+
provider: "email",
|
|
72
|
+
user: "QypOn5SQApyOPdUpIZsO9u2O",
|
|
73
|
+
email: "abc@test.com",
|
|
74
|
+
time: "2024-07-01T03:21:40Z",
|
|
75
|
+
});
|
|
76
|
+
const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
|
|
77
|
+
const req = await api.get("/", { headers: { authorization } });
|
|
78
|
+
expect(req).not.toSucceed("Credentials do not correspond to a user");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("cannot get the user", async () => {
|
|
82
|
+
store.set("auth:REqA2l022l8Q0tuIRtqLOPUy", {
|
|
83
|
+
id: "REqA2l022l8Q0tuIRtqLOPUy",
|
|
84
|
+
type: "token",
|
|
85
|
+
provider: "email",
|
|
86
|
+
user: "QypOn5SQApyOPdUpIZsO9u2O",
|
|
87
|
+
email: "abc@test.com",
|
|
88
|
+
time: "2024-07-01T03:21:40Z",
|
|
89
|
+
});
|
|
90
|
+
store.set("user:abc@test.com", {
|
|
91
|
+
id: "QypOn5SQApyOPdUpIZsO9u2O",
|
|
92
|
+
email: "abc@test.com",
|
|
93
|
+
});
|
|
94
|
+
const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
|
|
95
|
+
const req = await api.get("/", { headers: { authorization } });
|
|
96
|
+
expect(req).toSucceed();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("cookie", () => {
|
|
101
|
+
const store = kv(new Map());
|
|
102
|
+
const api = server({ store, auth: "cookie:email" })
|
|
103
|
+
.get("/", (ctx) => ctx.auth)
|
|
104
|
+
.test();
|
|
105
|
+
|
|
106
|
+
it("should have the proper token in email", async () => {
|
|
107
|
+
const cookie = "authorization=hello";
|
|
108
|
+
const req = await api.get("/", { headers: { cookie } });
|
|
109
|
+
expect(req).not.toSucceed("Invalid Authorization cookie");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("can get the proper session", async () => {
|
|
113
|
+
const cookie = "authorization=REqA2l022l8Q0tuIRtqLOPUy";
|
|
114
|
+
const req = await api.get("/", { headers: { cookie } });
|
|
115
|
+
expect(req).not.toSucceed("Invalid session");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("can get the proper session", async () => {
|
|
119
|
+
const cookie = "authorization=REqA2l022l8Q0tuIRtqLOPUy";
|
|
120
|
+
const req = await api.get("/", { headers: { cookie } });
|
|
121
|
+
expect(req).not.toSucceed();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cookies } from "../reply.js";
|
|
2
|
+
|
|
3
|
+
export default async function logout(ctx) {
|
|
4
|
+
const { id, type } = ctx.auth;
|
|
5
|
+
await ctx.options.auth.session.del(id);
|
|
6
|
+
|
|
7
|
+
if (type === "token") {
|
|
8
|
+
return { token: null };
|
|
9
|
+
} else if (type === "cookie") {
|
|
10
|
+
return cookies({ authorization: null }).send({});
|
|
11
|
+
} else if (type === "jwt") {
|
|
12
|
+
throw new Error("JWT auth not supported yet");
|
|
13
|
+
} else if (type === "key") {
|
|
14
|
+
throw new Error("Key auth not supported yet");
|
|
15
|
+
} else {
|
|
16
|
+
throw new Error("Unknown auth type");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import argon2 from "argon2";
|
|
2
|
+
|
|
3
|
+
import { createId } from "../../helpers/index.js";
|
|
4
|
+
import { ServerError, status } from "../../index.js";
|
|
5
|
+
|
|
6
|
+
const createSession = async (user, ctx) => {
|
|
7
|
+
const { type, session, cleanUser } = ctx.options.auth;
|
|
8
|
+
user = cleanUser(user);
|
|
9
|
+
const token = createId();
|
|
10
|
+
const provider = "email";
|
|
11
|
+
const time = new Date().toISOString().replace(/\.[0-9]*/, "");
|
|
12
|
+
ctx.auth = {
|
|
13
|
+
id: token,
|
|
14
|
+
type,
|
|
15
|
+
provider,
|
|
16
|
+
user: user.id,
|
|
17
|
+
email: user.email,
|
|
18
|
+
time,
|
|
19
|
+
};
|
|
20
|
+
await session.set(token, ctx.auth, { expires: "1w" });
|
|
21
|
+
|
|
22
|
+
if (type === "token") {
|
|
23
|
+
return status(201).json({ ...user, token });
|
|
24
|
+
} else if (type === "cookie") {
|
|
25
|
+
return status(201).cookies({ authorization: token }).send(user);
|
|
26
|
+
} else if (type === "jwt") {
|
|
27
|
+
throw new Error("JWT auth not supported yet");
|
|
28
|
+
} else if (type === "key") {
|
|
29
|
+
throw new Error("Key auth not supported yet");
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error("Unknown auth type");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function login(ctx) {
|
|
36
|
+
const { email, password } = ctx.body;
|
|
37
|
+
if (!email) throw ServerError.LOGIN_NO_EMAIL();
|
|
38
|
+
if (!/@/.test(email)) throw ServerError.LOGIN_INVALID_EMAIL();
|
|
39
|
+
if (!password) throw ServerError.LOGIN_NO_PASSWORD();
|
|
40
|
+
if (password.length < 8) throw ServerError.LOGIN_INVALID_PASSWORD();
|
|
41
|
+
|
|
42
|
+
const store = ctx.options.auth.store;
|
|
43
|
+
if (!(await store.has(email))) throw ServerError.LOGIN_WRONG_EMAIL();
|
|
44
|
+
|
|
45
|
+
const user = await store.get(email);
|
|
46
|
+
const isValid = await argon2.verify(user.password, password);
|
|
47
|
+
if (!isValid) throw ServerError.LOGIN_WRONG_PASSWORD();
|
|
48
|
+
|
|
49
|
+
return createSession(user, ctx);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function register(ctx) {
|
|
53
|
+
const { email, password, ...data } = ctx.body;
|
|
54
|
+
if (!email) throw ServerError.REGISTER_NO_EMAIL();
|
|
55
|
+
if (!/@/.test(email)) throw ServerError.REGISTER_INVALID_EMAIL();
|
|
56
|
+
if (!password) throw ServerError.REGISTER_NO_PASSWORD();
|
|
57
|
+
if (password.length < 8) throw ServerError.REGISTER_INVALID_PASSWORD();
|
|
58
|
+
|
|
59
|
+
const store = ctx.options.auth.store;
|
|
60
|
+
if (await store.has(email)) throw ServerError.REGISTER_EMAIL_EXISTS();
|
|
61
|
+
|
|
62
|
+
const user = {
|
|
63
|
+
id: createId(),
|
|
64
|
+
email,
|
|
65
|
+
password: await argon2.hash(password),
|
|
66
|
+
...data,
|
|
67
|
+
};
|
|
68
|
+
await store.set(email, user);
|
|
69
|
+
|
|
70
|
+
return createSession(user, ctx);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default { login, register };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import NoSession from "./NoSession.js";
|
|
2
|
+
|
|
3
|
+
export default async function session(ctx) {
|
|
4
|
+
const store = ctx.options.session?.store;
|
|
5
|
+
|
|
6
|
+
// If there's no store at all, we don't have session available;
|
|
7
|
+
// but that's okay, since it's only a problem if you try to use it
|
|
8
|
+
if (!store) return NoSession;
|
|
9
|
+
|
|
10
|
+
// There's a session cookie; use it as the key to get the data
|
|
11
|
+
// from the store
|
|
12
|
+
if (ctx.cookies.session) {
|
|
13
|
+
const session = await store.get(ctx.cookies.session);
|
|
14
|
+
if (session) return session;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {};
|
|
18
|
+
}
|
package/src/auth/user.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import ServerError from "../ServerError";
|
|
2
|
+
|
|
3
|
+
const findUser = async (auth, options) => {
|
|
4
|
+
if (!auth.provider) throw ServerError.AUTH_NO_PROVIDER();
|
|
5
|
+
if (!options.provider.includes(auth.provider)) {
|
|
6
|
+
const valid = JSON.stringify(options.provider);
|
|
7
|
+
throw ServerError.AUTH_INVALID_PROVIDER({ provider: auth.provider, valid });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (auth.provider === "email") {
|
|
11
|
+
return await options.store.get(auth.email);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default async function user(ctx) {
|
|
16
|
+
if (!ctx.auth) return;
|
|
17
|
+
|
|
18
|
+
const user = await findUser(ctx.auth, ctx.options.auth);
|
|
19
|
+
if (!user) throw ServerError.AUTH_NO_USER();
|
|
20
|
+
return ctx.options.auth.cleanUser(user);
|
|
21
|
+
}
|
package/src/context/node.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import auth from "../auth/index.js";
|
|
1
2
|
import { define } from "../helpers/index.js";
|
|
2
|
-
import findAuth from "./findAuth.js";
|
|
3
|
-
import findSession from "./findSession.js";
|
|
4
3
|
import parseBody from "./parseBody.js";
|
|
5
4
|
import parseCookies from "./parseCookies.js";
|
|
6
5
|
|
|
6
|
+
// https://stackoverflow.com/a/54029307/938236
|
|
7
|
+
const chunkArray = (arr, size) =>
|
|
8
|
+
arr.length > size
|
|
9
|
+
? [arr.slice(0, size), ...chunkArray(arr.slice(size), size)]
|
|
10
|
+
: [arr];
|
|
11
|
+
|
|
7
12
|
export default async (request, options = {}, app) => {
|
|
8
13
|
const ctx = {};
|
|
9
14
|
ctx.options = options;
|
|
@@ -11,10 +16,9 @@ export default async (request, options = {}, app) => {
|
|
|
11
16
|
ctx.res = { status: null, headers: {}, cookies: {} };
|
|
12
17
|
ctx.method = request.method.toLowerCase();
|
|
13
18
|
|
|
14
|
-
ctx.headers = request.
|
|
19
|
+
ctx.headers = parseHeaders(new Headers(chunkArray(request.rawHeaders, 2)));
|
|
15
20
|
ctx.cookies = parseCookies(ctx.headers.cookie);
|
|
16
|
-
|
|
17
|
-
await findAuth(ctx);
|
|
21
|
+
await auth.load(ctx);
|
|
18
22
|
|
|
19
23
|
const https = request.connection.encrypted ? "https" : "http";
|
|
20
24
|
const host = ctx.headers.host || "localhost" + options.port;
|
package/src/context/winter.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import findSession from "./findSession.js";
|
|
1
|
+
import auth from "../auth/index.js";
|
|
2
|
+
import { define, parseHeaders } from "../helpers/index.js";
|
|
4
3
|
import parseBody from "./parseBody.js";
|
|
5
4
|
import parseCookies from "./parseCookies.js";
|
|
6
5
|
|
|
@@ -11,10 +10,9 @@ export default async (request, options = {}, app) => {
|
|
|
11
10
|
ctx.res = { status: null, headers: {}, cookies: {} };
|
|
12
11
|
ctx.method = request.method.toLowerCase();
|
|
13
12
|
|
|
14
|
-
ctx.headers =
|
|
13
|
+
ctx.headers = parseHeaders(request.headers);
|
|
15
14
|
ctx.cookies = parseCookies(ctx.headers.cookie);
|
|
16
|
-
|
|
17
|
-
await findAuth(ctx);
|
|
15
|
+
await auth.load(ctx);
|
|
18
16
|
|
|
19
17
|
ctx.url = new URL(request.url.replace(/\/$/, ""));
|
|
20
18
|
define(ctx.url, "query", (url) =>
|
package/src/errors/index.js
CHANGED
|
@@ -6,6 +6,14 @@ ServerError.extend({
|
|
|
6
6
|
NO_STORE_READ: `You need a 'store' to read 'ctx.session.{key}'`,
|
|
7
7
|
AUTH_INVALID_TYPE: `Invalid Authorization type, '{type}'`,
|
|
8
8
|
AUTH_INVALID_TOKEN: `Invalid Authorization token`,
|
|
9
|
+
AUTH_INVALID_COOKIE: `Invalid Authorization cookie`,
|
|
10
|
+
AUTH_NO_PROVIDER: `No provider passed to the option "auth.provider"`,
|
|
11
|
+
AUTH_INVALID_PROVIDER: `Invalid provider "{provider}", valid ones are: {valid}`,
|
|
12
|
+
AUTH_NO_SESSION: { status: 401, message: "Invalid session" },
|
|
13
|
+
AUTH_NO_USER: {
|
|
14
|
+
status: 401,
|
|
15
|
+
message: "Credentials do not correspond to a user",
|
|
16
|
+
},
|
|
9
17
|
|
|
10
18
|
LOGIN_NO_EMAIL: "The email is required to log in",
|
|
11
19
|
LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
|
|
@@ -13,6 +21,12 @@ ServerError.extend({
|
|
|
13
21
|
LOGIN_INVALID_PASSWORD: "The email you wrote is not correct",
|
|
14
22
|
LOGIN_WRONG_ACCOUNT: `That email does not correspond to any account`,
|
|
15
23
|
LOGIN_WRONG_PASSWORD: `That is not the valid password`,
|
|
24
|
+
|
|
25
|
+
REGISTER_NO_EMAIL: `Email needed`,
|
|
26
|
+
REGISTER_INVALID_EMAIL: "The email you wrote is not correct",
|
|
27
|
+
REGISTER_NO_PASSWORD: `Password needed`,
|
|
28
|
+
REGISTER_INVALID_PASSWORD: "The email you wrote is not correct",
|
|
29
|
+
REGISTER_EMAIL_EXISTS: `Email is already registered`,
|
|
16
30
|
});
|
|
17
31
|
|
|
18
32
|
export default ServerError;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import server, { cookies } from "../index.js";
|
|
2
|
+
|
|
3
|
+
describe("set-cookie", () => {
|
|
4
|
+
const app = server()
|
|
5
|
+
.get("/hello", () => {
|
|
6
|
+
return cookies({ hello: "world" }).send();
|
|
7
|
+
})
|
|
8
|
+
.get("/multiple", () => {
|
|
9
|
+
return cookies({ a: "b", c: "d" }).send();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("sets the right cookie", async () => {
|
|
13
|
+
const api = app.test();
|
|
14
|
+
const res = await api.get("/hello");
|
|
15
|
+
expect(res.headers["set-cookie"]).toBe("hello=world");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("can set multiple cookies", async () => {
|
|
19
|
+
const api = app.test();
|
|
20
|
+
const res = await api.get("/multiple");
|
|
21
|
+
expect(res.headers["set-cookie"]).toEqual(["a=b", "c=d"]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Takes an object and returns a string with the proper cookie values
|
|
2
2
|
export default function createCookies(cookies) {
|
|
3
|
-
if (!cookies || !Object.keys(cookies).length) return
|
|
4
|
-
return Object.entries(cookies)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
3
|
+
if (!cookies || !Object.keys(cookies).length) return [];
|
|
4
|
+
return Object.entries(cookies).map(([key, val]) => {
|
|
5
|
+
if (!val) {
|
|
6
|
+
val = { value: "", expires: new Date(0).toUTCString() };
|
|
7
|
+
}
|
|
8
|
+
if (typeof val === "string") {
|
|
9
|
+
val = { value: val };
|
|
10
|
+
}
|
|
11
|
+
const { value, path, expires } = val;
|
|
12
|
+
const pathPart = path ? ";Path=" + path : "";
|
|
13
|
+
const expiresPart = expires ? ";Expires=" + expires : "";
|
|
14
|
+
return `${key}=${value || ""}${pathPart}${expiresPart}`;
|
|
15
|
+
});
|
|
13
16
|
}
|
package/src/helpers/index.js
CHANGED
|
@@ -4,4 +4,5 @@ export { default as define } from "./define.js";
|
|
|
4
4
|
export { default as getMachine } from "./getMachine.js";
|
|
5
5
|
export { default as handleRequest } from "./handleRequest.js";
|
|
6
6
|
export { default as iterate } from "./iterate.js";
|
|
7
|
+
export { default as parseHeaders } from "./parseHeaders.js";
|
|
7
8
|
export { default as types } from "./types.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default (raw) => {
|
|
2
|
+
const headers = {};
|
|
3
|
+
for (let [key, value] of raw.entries()) {
|
|
4
|
+
key = key.toLowerCase();
|
|
5
|
+
if (headers[key]) {
|
|
6
|
+
if (!Array.isArray(headers[key])) {
|
|
7
|
+
headers[key] = [headers[key]];
|
|
8
|
+
}
|
|
9
|
+
headers[key].push(value);
|
|
10
|
+
} else {
|
|
11
|
+
headers[key] = value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return headers;
|
|
15
|
+
};
|