@server/next 0.20.15 → 0.20.16
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 +3 -4
- package/readme.md +72 -67
- package/src/auth/providers/email.js +32 -6
- package/src/errors/index.js +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.16",
|
|
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"engineStrict": true,
|
|
32
32
|
"devDependencies": {
|
|
33
|
+
"argon2": "^0.40.3",
|
|
33
34
|
"jest": "^29.7.0",
|
|
34
35
|
"polystore": "^0.8.0"
|
|
35
36
|
},
|
|
@@ -37,7 +38,5 @@
|
|
|
37
38
|
"testEnvironment": "jest-environment-node",
|
|
38
39
|
"transform": {}
|
|
39
40
|
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"argon2": "^0.40.3"
|
|
42
|
-
}
|
|
41
|
+
"dependencies": {}
|
|
43
42
|
}
|
package/readme.md
CHANGED
|
@@ -1,116 +1,121 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Documentation
|
|
2
2
|
|
|
3
3
|
> **⚠️ WIP** This is an **experimental library** right now!
|
|
4
4
|
|
|
5
|
-
A fully-fledged web server for Bun and Node.js, with all the basics
|
|
5
|
+
A fully-fledged web server for Bun and Node.js, with all the basics built-in:
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
8
|
import server from "@server/next";
|
|
9
9
|
|
|
10
|
-
// Create a running instance of the server
|
|
11
10
|
export default server(options)
|
|
12
11
|
.get("/books", () => Book.list())
|
|
13
|
-
.post("/books",
|
|
12
|
+
.post("/books", BookSchema, (ctx) => {
|
|
14
13
|
return Book.create(ctx.body).save();
|
|
15
14
|
});
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
It includes all the things you would expect from a modern Server framework, like routing, static file serving, body+file parsing, gzip+brotli, streaming, testing, error handling, websockets, etc.
|
|
17
|
+
It includes all the things you would expect from a modern Server framework, like routing, static file serving, body+file parsing, gzip+brotli, streaming, testing, error handling, websockets, etc.
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
We also have integrations and adaptors for these:
|
|
20
|
+
|
|
21
|
+
- KV Stores: in-memory, Redis, Consul, DynamoDB, [Level](https://github.com/Level/level).
|
|
21
22
|
- Buckets: AWS S3, Cloudflare R2, Backblaze B2.
|
|
22
|
-
- Validation libraries: Zod, Joi, Yup.
|
|
23
|
+
- Validation libraries: [Zod](https://zod.dev/), [Joi](https://joi.dev/), [Yup](https://github.com/jquense/yup), [Validate](https://validatejs.org), etc.
|
|
24
|
+
- Auth: JWT, Session, Cookies, Social login.
|
|
23
25
|
|
|
24
26
|
```js
|
|
25
|
-
//
|
|
27
|
+
// How to test your server - index.test.js
|
|
26
28
|
import app from "./";
|
|
27
29
|
const api = app.test(); // Very convenient helper, AXIOS-like interface
|
|
28
30
|
|
|
29
|
-
it("can retrieve the
|
|
31
|
+
it("can retrieve the book list", async () => {
|
|
30
32
|
const { data: books } = await api.get("/books/");
|
|
31
33
|
expect(books[0]).toEqual({ id: 0, name: ... });
|
|
32
34
|
});
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
##
|
|
37
|
+
## Getting started
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
First install it:
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **[security]** Removed mandatory CSRF token, since this is only useful for server-rendered pages and not for SPA. Now we provide an `auth` module instead.
|
|
41
|
+
```
|
|
42
|
+
npm install @server/next
|
|
43
|
+
yarn add @server/next
|
|
44
|
+
bun install @server/next
|
|
45
|
+
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
Now you can create your first simple server:
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- The middleware can return:
|
|
52
|
-
- A number and it'll be set as the status code
|
|
53
|
-
- A string and it'll be sent as plain text or html (if it starts with "<")
|
|
54
|
-
- A readStream and it'll be piped to the response
|
|
55
|
-
- An object with `status`, `body` and `headers` and it'll be set raw.
|
|
56
|
-
- Response compression works
|
|
57
|
-
- Zod light integration
|
|
58
|
-
- Auth work
|
|
49
|
+
```js
|
|
50
|
+
// index.js
|
|
51
|
+
import server from "@server/next";
|
|
59
52
|
|
|
60
|
-
|
|
53
|
+
export default server()
|
|
54
|
+
.get("/", () => "Hello world")
|
|
55
|
+
.post("/", (ctx) => {
|
|
56
|
+
console.log(ctx.body);
|
|
57
|
+
return 201;
|
|
58
|
+
});
|
|
59
|
+
```
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
Then run `node .` or `bun .` and open your browser on http://localhost:3000/ to see the message.
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
There are some major configuration options that you might want to set up though, enumerated in the [Basic Usage](#basic-usage) and explained through the docs.
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
// createThumbnail.js
|
|
68
|
-
import sharp from "sharp";
|
|
65
|
+
## Basic usage
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
// Return a pipe, which will be streamed to the output
|
|
72
|
-
return sharp(ctx.url.params.name).resize(100, 100, { fit: "cover" }).png();
|
|
73
|
-
}
|
|
74
|
-
```
|
|
67
|
+
Now that you know how to create a barebones server, there are some important bits that you might want to update.
|
|
75
68
|
|
|
76
|
-
|
|
69
|
+
`SECRET`: create an `.env` file (ignored in git with `.gitignore`) with the `SECRET=` and then a long, unique random secret. It will be used to sign and/or encrypt things as needed.
|
|
77
70
|
|
|
78
|
-
`
|
|
71
|
+
`store`: almost anything you want to persist will need a KV store to do so. For dev you can use an in-memory or a file-based (easy for debugging!) store, but for production systems you would normally use something like Redis. Can be as easy as `const store = new Map();` for dev.
|
|
79
72
|
|
|
80
|
-
|
|
81
|
-
import server, { status, type, ...reply } from 'server';
|
|
73
|
+
> Note: `bucket` is still _not_ available
|
|
82
74
|
|
|
83
|
-
|
|
84
|
-
.use(mid1)
|
|
85
|
-
.get('/', cb1)
|
|
86
|
-
.get('/b', mid2, cb2)
|
|
87
|
-
.routes({ get: [['/c', mid3, cb3]] });
|
|
88
|
-
```
|
|
75
|
+
`bucket`: if you want to accept user files you will need a place to persist them. By default they are put in the filesystem, but since most cloud providers are ephemeral, provide a `bucket` and it will be used to handle the files directly.
|
|
89
76
|
|
|
90
|
-
|
|
77
|
+
An example of how that works in practice:
|
|
91
78
|
|
|
92
79
|
```js
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
return status(404).send("Not here..."); // treated as partial
|
|
96
|
-
return status(404); // treated as final
|
|
97
|
-
|
|
98
|
-
// NEW
|
|
99
|
-
return 404;
|
|
100
|
-
return status(404).send("Not here..."); // GOOD
|
|
101
|
-
return status(404).send(); // GOOD
|
|
102
|
-
|
|
103
|
-
// DON'T DO:
|
|
104
|
-
return status(404); // INVALID
|
|
105
|
-
```
|
|
80
|
+
// index.js
|
|
81
|
+
import server from "@server/next";
|
|
106
82
|
|
|
107
|
-
|
|
108
|
-
import
|
|
83
|
+
import Bucket from "bucket/b2";
|
|
84
|
+
import { createClient } from "redis";
|
|
109
85
|
|
|
110
|
-
|
|
86
|
+
const bucket = Bucket("mybucketname", { id, key });
|
|
87
|
+
const store = createClient("...").connect();
|
|
88
|
+
|
|
89
|
+
export default server({ bucket, store })
|
|
111
90
|
.get("/", () => "Hello world")
|
|
112
91
|
.post("/", (ctx) => {
|
|
113
92
|
console.log(ctx.body);
|
|
114
93
|
return 201;
|
|
115
94
|
});
|
|
116
95
|
```
|
|
96
|
+
|
|
97
|
+
## Guides
|
|
98
|
+
|
|
99
|
+
### Middleware
|
|
100
|
+
|
|
101
|
+
### Validation
|
|
102
|
+
|
|
103
|
+
### Stores
|
|
104
|
+
|
|
105
|
+
### File handling
|
|
106
|
+
|
|
107
|
+
## Options
|
|
108
|
+
|
|
109
|
+
Options docs here
|
|
110
|
+
|
|
111
|
+
## Router
|
|
112
|
+
|
|
113
|
+
Router docs here
|
|
114
|
+
|
|
115
|
+
## Context
|
|
116
|
+
|
|
117
|
+
Context docs here
|
|
118
|
+
|
|
119
|
+
## Reply
|
|
120
|
+
|
|
121
|
+
Reply docs here
|
|
@@ -1,10 +1,36 @@
|
|
|
1
|
-
import argon2 from "argon2";
|
|
2
|
-
|
|
3
1
|
import { createId } from "../../helpers/index.js";
|
|
4
2
|
import { ServerError, status } from "../../index.js";
|
|
5
3
|
import findUser from "../findUser.js";
|
|
6
4
|
import updateUser from "../updateUser.js";
|
|
7
5
|
|
|
6
|
+
const hash = new Proxy(
|
|
7
|
+
{},
|
|
8
|
+
{
|
|
9
|
+
get: (self, key) => {
|
|
10
|
+
const load = async () =>
|
|
11
|
+
Object.assign(
|
|
12
|
+
self,
|
|
13
|
+
await import("argon2").catch(() => {
|
|
14
|
+
throw new ServerError.AUTH_ARGON_NEEDED();
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
if (key === "verify" && !self.verify) {
|
|
18
|
+
return async (hash, pass) => {
|
|
19
|
+
await load();
|
|
20
|
+
return self.verify(hash, pass);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (key === "hash" && !self.hash) {
|
|
24
|
+
return async (pass) => {
|
|
25
|
+
await load();
|
|
26
|
+
return self.hash(pass);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return self[key];
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
8
34
|
const createSession = async (user, ctx) => {
|
|
9
35
|
const { type, session, cleanUser } = ctx.options.auth;
|
|
10
36
|
user = cleanUser(user);
|
|
@@ -45,7 +71,7 @@ async function login(ctx) {
|
|
|
45
71
|
if (!(await store.has(email))) throw ServerError.LOGIN_WRONG_EMAIL();
|
|
46
72
|
|
|
47
73
|
const user = await store.get(email);
|
|
48
|
-
const isValid = await
|
|
74
|
+
const isValid = await hash.verify(user.password, password);
|
|
49
75
|
if (!isValid) throw ServerError.LOGIN_WRONG_PASSWORD();
|
|
50
76
|
|
|
51
77
|
return createSession(user, ctx);
|
|
@@ -64,7 +90,7 @@ async function register(ctx) {
|
|
|
64
90
|
const user = {
|
|
65
91
|
id: createId(),
|
|
66
92
|
email,
|
|
67
|
-
password: await
|
|
93
|
+
password: await hash.hash(password),
|
|
68
94
|
...data,
|
|
69
95
|
};
|
|
70
96
|
await store.set(email, user);
|
|
@@ -99,10 +125,10 @@ async function password(ctx) {
|
|
|
99
125
|
|
|
100
126
|
const fullUser = await findUser(ctx.auth, ctx.options.auth.store);
|
|
101
127
|
|
|
102
|
-
const isValid = await
|
|
128
|
+
const isValid = await hash.verify(fullUser.password, previous);
|
|
103
129
|
if (!isValid) throw ServerError.LOGIN_WRONG_PASSWORD();
|
|
104
130
|
|
|
105
|
-
fullUser.password = await
|
|
131
|
+
fullUser.password = await hash.hash(updated);
|
|
106
132
|
await updateUser(fullUser, ctx.auth, ctx.options.auth.store);
|
|
107
133
|
|
|
108
134
|
return 200;
|
package/src/errors/index.js
CHANGED
|
@@ -4,6 +4,9 @@ ServerError.extend({
|
|
|
4
4
|
NO_STORE: `You need a 'store' to write 'ctx.session'`,
|
|
5
5
|
NO_STORE_WRITE: `You need a 'store' to write 'ctx.session.{key}'`,
|
|
6
6
|
NO_STORE_READ: `You need a 'store' to read 'ctx.session.{key}'`,
|
|
7
|
+
|
|
8
|
+
AUTH_ARGON_NEEDED:
|
|
9
|
+
"Argon2 is needed for the auth module, please install it with 'npm i argon2'",
|
|
7
10
|
AUTH_INVALID_TYPE: `Invalid Authorization type, '{type}'`,
|
|
8
11
|
AUTH_INVALID_TOKEN: `Invalid Authorization token`,
|
|
9
12
|
AUTH_INVALID_COOKIE: `Invalid Authorization cookie`,
|
|
@@ -18,14 +21,14 @@ ServerError.extend({
|
|
|
18
21
|
LOGIN_NO_EMAIL: "The email is required to log in",
|
|
19
22
|
LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
|
|
20
23
|
LOGIN_NO_PASSWORD: "The email is required to log in",
|
|
21
|
-
LOGIN_INVALID_PASSWORD: "The
|
|
24
|
+
LOGIN_INVALID_PASSWORD: "The password you wrote is not correct",
|
|
22
25
|
LOGIN_WRONG_ACCOUNT: `That email does not correspond to any account`,
|
|
23
26
|
LOGIN_WRONG_PASSWORD: `That is not the valid password`,
|
|
24
27
|
|
|
25
28
|
REGISTER_NO_EMAIL: `Email needed`,
|
|
26
29
|
REGISTER_INVALID_EMAIL: "The email you wrote is not correct",
|
|
27
30
|
REGISTER_NO_PASSWORD: `Password needed`,
|
|
28
|
-
REGISTER_INVALID_PASSWORD: "The
|
|
31
|
+
REGISTER_INVALID_PASSWORD: "The password you wrote is not correct",
|
|
29
32
|
REGISTER_EMAIL_EXISTS: `Email is already registered`,
|
|
30
33
|
});
|
|
31
34
|
|