@server/next 0.20.9 → 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/createCookies.js +8 -3
- package/src/helpers/index.js +1 -0
- package/src/helpers/parseHeaders.js +15 -0
- package/src/index.js +27 -14
- package/src/index.test.js +7 -4
- package/src/middle/assets.js +12 -0
- package/src/middle/index.js +7 -66
- package/src/test/toSucceed.js +64 -0
- package/src/auth.test.js +0 -62
- 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;
|
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
export default function createCookies(cookies) {
|
|
3
3
|
if (!cookies || !Object.keys(cookies).length) return [];
|
|
4
4
|
return Object.entries(cookies).map(([key, val]) => {
|
|
5
|
+
if (!val) {
|
|
6
|
+
val = { value: "", expires: new Date(0).toUTCString() };
|
|
7
|
+
}
|
|
5
8
|
if (typeof val === "string") {
|
|
6
|
-
val = { value: val
|
|
9
|
+
val = { value: val };
|
|
7
10
|
}
|
|
8
|
-
const { value, path } = val;
|
|
9
|
-
|
|
11
|
+
const { value, path, expires } = val;
|
|
12
|
+
const pathPart = path ? ";Path=" + path : "";
|
|
13
|
+
const expiresPart = expires ? ";Expires=" + expires : "";
|
|
14
|
+
return `${key}=${value || ""}${pathPart}${expiresPart}`;
|
|
10
15
|
});
|
|
11
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
|
+
};
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getMachine,
|
|
11
11
|
handleRequest,
|
|
12
12
|
iterate,
|
|
13
|
+
parseHeaders,
|
|
13
14
|
} from "./helpers/index.js";
|
|
14
15
|
import middle from "./middle/index.js";
|
|
15
16
|
|
|
@@ -59,13 +60,34 @@ const validateOptions = (options, env = {}) => {
|
|
|
59
60
|
if (options.store && options.cookies) {
|
|
60
61
|
options.session = { store: options.store.prefix("session:") };
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
|
|
64
|
+
// AUTH
|
|
65
|
+
options.auth = options.auth || env.AUTH || null;
|
|
63
66
|
if (options.auth) {
|
|
64
67
|
if (typeof options.auth !== "object") {
|
|
65
|
-
|
|
68
|
+
const [type, provider] = options.auth.split(":");
|
|
69
|
+
options.auth = { type, provider };
|
|
70
|
+
}
|
|
71
|
+
if (typeof options.auth.provider === "string") {
|
|
72
|
+
options.auth.provider === options.auth.provider.split("|");
|
|
73
|
+
}
|
|
74
|
+
if (!options.auth.type) {
|
|
75
|
+
throw new Error("Auth options needs a type");
|
|
76
|
+
}
|
|
77
|
+
if (!options.auth.provider) {
|
|
78
|
+
throw new Error("Auth options needs a provider");
|
|
79
|
+
}
|
|
80
|
+
if (!options.auth.session && options.store) {
|
|
81
|
+
options.auth.session = options.store.prefix("auth:");
|
|
66
82
|
}
|
|
67
83
|
if (!options.auth.store && options.store) {
|
|
68
|
-
options.auth.store = options.store.prefix("
|
|
84
|
+
options.auth.store = options.store.prefix("user:");
|
|
85
|
+
}
|
|
86
|
+
if (!options.auth.cleanUser) {
|
|
87
|
+
options.auth.cleanUser = (fullUser) => {
|
|
88
|
+
const { password, ...user } = fullUser;
|
|
89
|
+
return user;
|
|
90
|
+
};
|
|
69
91
|
}
|
|
70
92
|
}
|
|
71
93
|
|
|
@@ -212,17 +234,7 @@ server.prototype.test = function () {
|
|
|
212
234
|
new Request("http://localhost:3000" + path, options)
|
|
213
235
|
);
|
|
214
236
|
|
|
215
|
-
const headers =
|
|
216
|
-
for (const [key, value] of res.headers.entries()) {
|
|
217
|
-
if (headers[key]) {
|
|
218
|
-
if (!Array.isArray(headers[key])) {
|
|
219
|
-
headers[key] = [headers[key]];
|
|
220
|
-
}
|
|
221
|
-
headers[key].push(value);
|
|
222
|
-
} else {
|
|
223
|
-
headers[key] = value;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
237
|
+
const headers = parseHeaders(res.headers);
|
|
226
238
|
let data;
|
|
227
239
|
if (headers["set-cookie"]) {
|
|
228
240
|
// TODO: this should really be a smart merge of the 2
|
|
@@ -236,6 +248,7 @@ server.prototype.test = function () {
|
|
|
236
248
|
return { status: res.status, headers, data };
|
|
237
249
|
};
|
|
238
250
|
return {
|
|
251
|
+
app: this,
|
|
239
252
|
get: (path, options) => fetch(path, { method: "get", ...options }),
|
|
240
253
|
head: (path, options) => fetch(path, { method: "head", ...options }),
|
|
241
254
|
post: (path, body, options) =>
|
package/src/index.test.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "./test/toSucceed.js";
|
|
2
|
+
|
|
1
3
|
import server, { status } from "./index.js";
|
|
2
4
|
|
|
3
5
|
describe("return different types", () => {
|
|
@@ -20,13 +22,14 @@ describe("return different types", () => {
|
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
it("can get the object", async () => {
|
|
23
|
-
const
|
|
24
|
-
expect(
|
|
25
|
+
const req = await api.get("/object");
|
|
26
|
+
expect(req).toSucceed({ hello: "world" });
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
it("can get the status", async () => {
|
|
28
|
-
const
|
|
29
|
-
expect(
|
|
30
|
+
const req = await api.get("/status");
|
|
31
|
+
expect(req).toSucceed();
|
|
32
|
+
expect(req.status).toBe(201);
|
|
30
33
|
});
|
|
31
34
|
});
|
|
32
35
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type } from "../reply.js";
|
|
2
|
+
|
|
3
|
+
export default async function assets(ctx) {
|
|
4
|
+
try {
|
|
5
|
+
// TODO: streaming
|
|
6
|
+
const asset = await ctx.options.public.read(ctx.url.pathname);
|
|
7
|
+
if (!asset) return;
|
|
8
|
+
return type(ctx.url.pathname.split(".").pop()).send(asset);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
// NO-OP; if there's no file, keep going the normal flow
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/middle/index.js
CHANGED
|
@@ -1,71 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import auth from "../auth/index.js";
|
|
2
|
+
import assets from "./assets.js";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import ServerError from "../ServerError.js";
|
|
6
|
-
|
|
7
|
-
export default function middle(ctx) {
|
|
4
|
+
export default async function middle(ctx) {
|
|
5
|
+
// Serve assets
|
|
8
6
|
if (ctx.options.public) {
|
|
9
|
-
|
|
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
|
-
]);
|
|
7
|
+
// We need these before other endpoints
|
|
8
|
+
ctx.app.handlers.get.unshift(["*", "*", assets]);
|
|
21
9
|
}
|
|
22
10
|
|
|
23
|
-
|
|
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
|
-
}
|
|
11
|
+
await auth.middle(ctx);
|
|
71
12
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { expect } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
const reset = "\x1b[0m";
|
|
4
|
+
|
|
5
|
+
const spaceOrEnter = (msg) => {
|
|
6
|
+
if (typeof msg === "string") {
|
|
7
|
+
return " ";
|
|
8
|
+
}
|
|
9
|
+
return "\n";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function toSucceed(request, message) {
|
|
13
|
+
const pass = request.status >= 200 && request.status < 300;
|
|
14
|
+
|
|
15
|
+
if (message && JSON.stringify(request.data) !== JSON.stringify(message)) {
|
|
16
|
+
if (pass) {
|
|
17
|
+
return {
|
|
18
|
+
message: () =>
|
|
19
|
+
`${reset}Expected body:${spaceOrEnter(
|
|
20
|
+
message
|
|
21
|
+
)}${this.utils.printExpected(message)}\nReceived body:${spaceOrEnter(
|
|
22
|
+
request.data
|
|
23
|
+
)}${this.utils.printReceived(request.data)}`,
|
|
24
|
+
pass,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
message: () =>
|
|
30
|
+
`${reset}Expected error:${spaceOrEnter(
|
|
31
|
+
message
|
|
32
|
+
)}${this.utils.printExpected(message)}\nReceived error:${spaceOrEnter(
|
|
33
|
+
request.data
|
|
34
|
+
)}${this.utils.printReceived(request.data)}`,
|
|
35
|
+
pass: !pass,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (pass) {
|
|
40
|
+
return {
|
|
41
|
+
message: () =>
|
|
42
|
+
`${reset}Expected ${this.utils.printExpected(
|
|
43
|
+
request.status
|
|
44
|
+
)} to be an error code, received body:\n${this.utils.printExpected(
|
|
45
|
+
request.data
|
|
46
|
+
)}`,
|
|
47
|
+
pass: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
message: () =>
|
|
53
|
+
`${reset}Expected ${this.utils.printReceived(
|
|
54
|
+
request.status
|
|
55
|
+
)} to succeed, received body:\n${this.utils.printReceived(
|
|
56
|
+
request.data,
|
|
57
|
+
null,
|
|
58
|
+
2
|
|
59
|
+
)}`,
|
|
60
|
+
pass: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
expect.extend({ toSucceed });
|
package/src/auth.test.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import kv from "polystore";
|
|
2
|
-
|
|
3
|
-
import server from "./index.js";
|
|
4
|
-
|
|
5
|
-
describe("auth", () => {
|
|
6
|
-
const store = kv(new Map());
|
|
7
|
-
const api = server({ store })
|
|
8
|
-
.get("/", (ctx) => ctx.headers.authorization)
|
|
9
|
-
.test();
|
|
10
|
-
|
|
11
|
-
it("should be Bearer", async () => {
|
|
12
|
-
const authorization = "Basic REqA2l022l8Q0tuIRtqLOPUy";
|
|
13
|
-
const { data } = await api.get("/", { headers: { authorization } });
|
|
14
|
-
expect(data).toBe("Invalid Authorization type, 'Basic'");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("should have the proper token", async () => {
|
|
18
|
-
const authorization = "Bearer hola";
|
|
19
|
-
const { data } = await api.get("/", { headers: { authorization } });
|
|
20
|
-
expect(data).toBe("Invalid Authorization token");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("can get the nested get", async () => {
|
|
24
|
-
const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
|
|
25
|
-
const { data } = await api.get("/", { headers: { authorization } });
|
|
26
|
-
expect(data).toBe("Bearer REqA2l022l8Q0tuIRtqLOPUy");
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("user creation flow", () => {
|
|
31
|
-
// These are obviously mock data
|
|
32
|
-
const EMAIL = "abc@test.com";
|
|
33
|
-
const PASS = "11111111";
|
|
34
|
-
const CREDENTIALS = { email: EMAIL, password: PASS };
|
|
35
|
-
|
|
36
|
-
const store = kv(new Map());
|
|
37
|
-
const auth = { type: "token", providers: "email" };
|
|
38
|
-
const api = server({ auth, store }).test();
|
|
39
|
-
|
|
40
|
-
it("can create a new user", async () => {
|
|
41
|
-
const register = await api.post("/auth/register/email", CREDENTIALS);
|
|
42
|
-
expect(register.status).toBe(201);
|
|
43
|
-
expect(await store.keys()).toEqual([
|
|
44
|
-
"auth:abc@test.com",
|
|
45
|
-
"session:" + register.data.token,
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
const authorization = "Bearer " + register.data.token;
|
|
49
|
-
const headers = { authorization };
|
|
50
|
-
const logout = await api.post("/auth/logout", {}, { headers });
|
|
51
|
-
|
|
52
|
-
expect(logout.status).toBe(200);
|
|
53
|
-
expect(await store.keys()).toEqual(["auth:abc@test.com"]);
|
|
54
|
-
|
|
55
|
-
const login = await api.post("/auth/login/email", CREDENTIALS);
|
|
56
|
-
expect(login.status).toBe(200);
|
|
57
|
-
expect(await store.keys()).toEqual([
|
|
58
|
-
"auth:abc@test.com",
|
|
59
|
-
"session:" + login.data.token,
|
|
60
|
-
]);
|
|
61
|
-
});
|
|
62
|
-
});
|
package/src/context/findAuth.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { ServerError } from "../index.js";
|
|
2
|
-
|
|
3
|
-
export default async function findAuth(ctx) {
|
|
4
|
-
// NO AUTH AT ALL
|
|
5
|
-
// If there's no even auth option, nothing to do
|
|
6
|
-
const store = ctx.options.auth?.store;
|
|
7
|
-
if (!store) return;
|
|
8
|
-
|
|
9
|
-
// AUTHENTICATION IS AVAILABLE
|
|
10
|
-
ctx.auth = {
|
|
11
|
-
type: ctx.options.auth.type,
|
|
12
|
-
providers: ctx.options.auth.providers,
|
|
13
|
-
store,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// If the user is not authenticated, there's no auth to retrieve
|
|
17
|
-
if (!ctx.headers.authorization) return;
|
|
18
|
-
|
|
19
|
-
// AUTHENTICATED REQUEST
|
|
20
|
-
// Check the authentication header
|
|
21
|
-
const [type, id] = ctx.headers.authorization.trim().split(" ");
|
|
22
|
-
if (type.toLowerCase() !== "bearer") {
|
|
23
|
-
throw ServerError.AUTH_INVALID_TYPE({ type });
|
|
24
|
-
}
|
|
25
|
-
if (id.length !== 24) {
|
|
26
|
-
throw ServerError.AUTH_INVALID_TOKEN();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Extend the basics
|
|
30
|
-
ctx.auth.id = id;
|
|
31
|
-
ctx.user = await store.get(id);
|
|
32
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { ServerError } from "../index.js";
|
|
2
|
-
|
|
3
|
-
class NoSession {}
|
|
4
|
-
|
|
5
|
-
export default async function findSession(ctx) {
|
|
6
|
-
const store = ctx.options.session?.store;
|
|
7
|
-
|
|
8
|
-
// If there's no store at all, we don't have session available;
|
|
9
|
-
// but that's okay, since it's only a problem if you try to use it
|
|
10
|
-
if (!store) {
|
|
11
|
-
return new Proxy(new NoSession(), {
|
|
12
|
-
get(target, key) {
|
|
13
|
-
if (target[key]) return target[key];
|
|
14
|
-
if (key === "then") return target[key];
|
|
15
|
-
throw ServerError.NO_STORE_READ({ key });
|
|
16
|
-
},
|
|
17
|
-
set(target, key, value) {
|
|
18
|
-
if (target[key] || key === "then") {
|
|
19
|
-
target[key] = value;
|
|
20
|
-
} else {
|
|
21
|
-
throw ServerError.NO_STORE_WRITE({ key });
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// There's a session cookie; use it as the key to get the data
|
|
28
|
-
// from the store
|
|
29
|
-
if (ctx.cookies.session) {
|
|
30
|
-
return (await store.get(ctx.cookies.session)) || {};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {};
|
|
34
|
-
}
|