@stanlemon/server-with-auth 0.2.11 → 0.3.0
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/README.md +48 -0
- package/app.js +37 -4
- package/package.json +8 -4
- package/src/constants.js +59 -0
- package/src/createAppServer.js +71 -29
- package/src/data/knex-user-dao.js +142 -0
- package/src/data/lowdb-user-dao.js +79 -49
- package/src/data/user-dao.js +38 -11
- package/src/data/user-dao.test.js +156 -0
- package/src/index.js +5 -4
- package/src/routes/auth.js +221 -48
- package/src/routes/auth.test.js +204 -49
- package/src/schema/index.js +70 -0
- package/src/utilities/checkSchemas.js +25 -0
- package/src/utilities/checkUserDao.js +7 -0
- package/test.http +62 -4
- package/src/data/lowdb-user-dao.test.js +0 -111
- package/src/schema/user.js +0 -8
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Express App Server with Authentication
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40stanlemon%2Fserver-with-auth)
|
|
4
|
+
|
|
5
|
+
This is a base express app server that is wired up with sensible defaults, like compression, json support and serving the `dist` folder statically and also includes basic authentication support. It builds off of the [@stanlemon/server](../server/README.md) package.
|
|
6
|
+
|
|
7
|
+
This package includes authentication against secure endpoints using JWT. There are endpoints for logging in and signing up, as well as basic user management flows such as updating a profile, verifying a user and resetting a password.
|
|
8
|
+
|
|
9
|
+
When `NODE_ENV=development` the server will also proxy requests to webpack.
|
|
10
|
+
|
|
11
|
+
This library goes well with [@stanlemon/webdev](../webdev/README.md). You can see this package, along with [@stanlemon/webdev] in action by using the [@stanlemon/app-template](../../apps/template/README.md) package.
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import {
|
|
15
|
+
createAppServer,
|
|
16
|
+
asyncJsonHandler as handler,
|
|
17
|
+
NotFoundException,
|
|
18
|
+
LowDBUserDao,
|
|
19
|
+
createLowDb
|
|
20
|
+
} from "@stanlemon/server-with-auth";
|
|
21
|
+
import from "./src/data/lowdb-user-dao.js";
|
|
22
|
+
|
|
23
|
+
const db = createLowDb();
|
|
24
|
+
const dao = new LowDBUserDao(db);
|
|
25
|
+
|
|
26
|
+
const app = createAppServer({
|
|
27
|
+
port: 3003,secure: ["/api/"],
|
|
28
|
+
schemas,
|
|
29
|
+
dao,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// Insecure endpoint
|
|
34
|
+
app.get(
|
|
35
|
+
"/",
|
|
36
|
+
handler(() => ({ hello: "world" }))
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Secure endpoint
|
|
40
|
+
app.get(
|
|
41
|
+
"/api/users",
|
|
42
|
+
handler(() => ({
|
|
43
|
+
users: db.data.users.map((u) =>
|
|
44
|
+
omit(u, ["password", "verification_token"])
|
|
45
|
+
),
|
|
46
|
+
}))
|
|
47
|
+
);
|
|
48
|
+
```
|
package/app.js
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { omit } from "lodash-es";
|
|
2
|
+
import EventEmitter from "node:events";
|
|
3
|
+
import Joi from "joi";
|
|
4
|
+
import {
|
|
5
|
+
createAppServer,
|
|
6
|
+
asyncJsonHandler as handler,
|
|
7
|
+
createSchemas,
|
|
8
|
+
EVENTS,
|
|
9
|
+
LowDBUserDao,
|
|
10
|
+
createLowDb,
|
|
11
|
+
} from "./src/index.js";
|
|
3
12
|
|
|
4
|
-
const db =
|
|
13
|
+
const db = createLowDb();
|
|
5
14
|
const dao = new LowDBUserDao(db);
|
|
15
|
+
const eventEmitter = new EventEmitter();
|
|
16
|
+
Object.values(EVENTS).forEach((event) => {
|
|
17
|
+
eventEmitter.on(event, (user) => {
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.info(
|
|
20
|
+
`Event = ${event}, User = ${JSON.stringify(
|
|
21
|
+
omit(user, ["password", "verification_token"])
|
|
22
|
+
)}`
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const schemas = createSchemas({
|
|
28
|
+
fullName: Joi.string().required().label("Full Name"),
|
|
29
|
+
email: Joi.string().email().required().label("Email"),
|
|
30
|
+
});
|
|
6
31
|
|
|
7
32
|
const app = createAppServer({
|
|
8
33
|
port: 3003,
|
|
9
34
|
secure: ["/api/"],
|
|
35
|
+
schemas,
|
|
10
36
|
dao,
|
|
37
|
+
eventEmitter,
|
|
11
38
|
});
|
|
12
39
|
|
|
13
40
|
// Insecure endpoint
|
|
@@ -25,5 +52,11 @@ app.get(
|
|
|
25
52
|
// Secure endpoint
|
|
26
53
|
app.get(
|
|
27
54
|
"/api/users",
|
|
28
|
-
handler(() => ({
|
|
55
|
+
handler(() => ({
|
|
56
|
+
users: db.data.users.map((u) =>
|
|
57
|
+
omit(u, ["password", "verification_token"])
|
|
58
|
+
),
|
|
59
|
+
}))
|
|
29
60
|
);
|
|
61
|
+
|
|
62
|
+
app.catch404s("/api/*");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stanlemon/server-with-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A basic express web server setup with authentication baked in.",
|
|
5
5
|
"author": "Stan Lemon <stanlemon@users.noreply.github.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "NODE_ENV=development nodemon --ignore db.json ./app.js",
|
|
15
15
|
"lint": "eslint --ext js,jsx,ts,tsx ./",
|
|
16
|
-
"lint:
|
|
16
|
+
"lint:fix": "eslint --fix --ext js,jsx,ts,tsx ./",
|
|
17
17
|
"test": "jest --detectOpenHandles",
|
|
18
18
|
"test:coverage": "jest --detectOpenHandles --coverage",
|
|
19
19
|
"test:watch": "jest --detectOpenHandles --watch"
|
|
@@ -22,17 +22,21 @@
|
|
|
22
22
|
"@stanlemon/server": "*",
|
|
23
23
|
"@stanlemon/webdev": "*",
|
|
24
24
|
"bcryptjs": "^2.4.3",
|
|
25
|
+
"express-session": "^1.17.3",
|
|
25
26
|
"jsonwebtoken": "^9.0.2",
|
|
26
|
-
"lowdb": "^
|
|
27
|
+
"lowdb": "^7.0.1",
|
|
27
28
|
"lowdb-node": "^3.0.2",
|
|
28
29
|
"passport": "^0.7.0",
|
|
29
30
|
"passport-jwt": "^4.0.1",
|
|
31
|
+
"passport-local": "^1.0.0",
|
|
30
32
|
"uuid": "^9.0.1"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
35
|
"@stanlemon/eslint-config": "*",
|
|
34
36
|
"@types/supertest": "^6.0.2",
|
|
37
|
+
"better-sqlite3": "^9.2.2",
|
|
38
|
+
"knex": "^3.1.0",
|
|
35
39
|
"nodemon": "^3.0.2",
|
|
36
40
|
"supertest": "^6.3.3"
|
|
37
41
|
}
|
|
38
|
-
}
|
|
42
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const HIDDEN_FIELDS = ["password", "verification_token"];
|
|
2
|
+
|
|
3
|
+
export const ROUTES = {
|
|
4
|
+
SIGNUP: "/auth/signup",
|
|
5
|
+
REGISTER: "/auth/register",
|
|
6
|
+
SESSION: "/auth/session",
|
|
7
|
+
LOGIN: "/auth/login",
|
|
8
|
+
LOGOUT: "/auth/logout",
|
|
9
|
+
USER: "/auth/user",
|
|
10
|
+
VERIFY: "/auth/verify/:token",
|
|
11
|
+
RESET: "/auth/reset",
|
|
12
|
+
PASSWORD: "/auth/password",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Authentication related events.
|
|
17
|
+
*
|
|
18
|
+
* Listen to these with the {EventEmitter}.
|
|
19
|
+
*/
|
|
20
|
+
export const EVENTS = {
|
|
21
|
+
/**
|
|
22
|
+
* When a user logs in.
|
|
23
|
+
*/
|
|
24
|
+
USER_LOGIN: "user.login",
|
|
25
|
+
/**
|
|
26
|
+
* When a user logs out.
|
|
27
|
+
*/
|
|
28
|
+
USER_LOGOUT: "user.logout",
|
|
29
|
+
/**
|
|
30
|
+
* When a user is created.
|
|
31
|
+
*/
|
|
32
|
+
USER_CREATED: "user.created",
|
|
33
|
+
/**
|
|
34
|
+
* When a user is verified.
|
|
35
|
+
*/
|
|
36
|
+
USER_VERIFIED: "user.verified",
|
|
37
|
+
/**
|
|
38
|
+
* When a user is updated.
|
|
39
|
+
*/
|
|
40
|
+
USER_UPDATED: "user.updated",
|
|
41
|
+
/**
|
|
42
|
+
* When a user is deleted.
|
|
43
|
+
*/
|
|
44
|
+
USER_DELETED: "user.deleted",
|
|
45
|
+
/**
|
|
46
|
+
* When a user changes their password.
|
|
47
|
+
*/
|
|
48
|
+
USER_PASSWORD: "user.password",
|
|
49
|
+
/**
|
|
50
|
+
* When a user reset is requested.
|
|
51
|
+
* This is not a managed lifecycle event, you'll need to implement this!
|
|
52
|
+
*/
|
|
53
|
+
USER_RESET_REQUESTED: "user.reset.requested",
|
|
54
|
+
/**
|
|
55
|
+
* When a user reset is completed.
|
|
56
|
+
* This is not a managed lifecycle event, you'll need to implement this!
|
|
57
|
+
*/
|
|
58
|
+
USER_RESET_COMPLETED: "user.reset.completed",
|
|
59
|
+
};
|
package/src/createAppServer.js
CHANGED
|
@@ -1,54 +1,60 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
1
2
|
import dotenv from "dotenv";
|
|
2
3
|
import {
|
|
3
4
|
createAppServer as createBaseAppServer,
|
|
4
5
|
DEFAULTS as BASE_DEFAULTS,
|
|
5
6
|
} from "@stanlemon/server";
|
|
7
|
+
import expressSession from "express-session";
|
|
6
8
|
import passport from "passport";
|
|
9
|
+
import { Strategy as LocalStrategy } from "passport-local";
|
|
7
10
|
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
|
|
8
11
|
import { v4 as uuid } from "uuid";
|
|
9
|
-
import
|
|
10
|
-
import defaultUserSchema from "./schema/user.js";
|
|
12
|
+
import SCHEMAS from "./schema/index.js";
|
|
11
13
|
import checkAuth from "./checkAuth.js";
|
|
12
14
|
import auth from "./routes/auth.js";
|
|
13
15
|
import UserDao from "./data/user-dao.js";
|
|
16
|
+
import checkUserDao from "./utilities/checkUserDao.js";
|
|
17
|
+
import checkSchemas from "./utilities/checkSchemas.js";
|
|
14
18
|
|
|
15
19
|
dotenv.config();
|
|
16
20
|
|
|
17
21
|
export const DEFAULTS = {
|
|
18
22
|
...BASE_DEFAULTS,
|
|
19
23
|
secure: [],
|
|
20
|
-
|
|
24
|
+
schemas: SCHEMAS,
|
|
21
25
|
dao: new UserDao(),
|
|
26
|
+
eventEmitter: new EventEmitter(),
|
|
27
|
+
jwtExpireInMinutes: 10,
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* Create an app server with authentication.
|
|
32
|
+
* @param {object} options
|
|
26
33
|
* @param {number} options.port Port to listen on
|
|
27
34
|
* @param {boolean} options.webpack Whether or not to create a proxy for webpack
|
|
28
35
|
* @param {string[]} options.secure Paths that require authentication
|
|
29
|
-
* @param {Joi.Schema} options.
|
|
36
|
+
* @param {Object.<string, Joi.Schema>} options.schemas Object map of routes to schema for validating route inputs.
|
|
30
37
|
* @param {UserDao} options.dao Data access object for user interactions
|
|
31
|
-
* @returns {import("
|
|
38
|
+
* @returns {import("@stanlemon/server/src/createAppServer.js").AppServer} Pre-configured express app server with extra helper methods
|
|
32
39
|
*/
|
|
40
|
+
/* eslint-disable max-lines-per-function */
|
|
33
41
|
export default function createAppServer(options) {
|
|
34
|
-
const {
|
|
42
|
+
const {
|
|
43
|
+
port,
|
|
44
|
+
webpack,
|
|
45
|
+
start,
|
|
46
|
+
secure,
|
|
47
|
+
schemas,
|
|
48
|
+
dao,
|
|
49
|
+
eventEmitter,
|
|
50
|
+
jwtExpireInMinutes,
|
|
51
|
+
} = {
|
|
35
52
|
...DEFAULTS,
|
|
36
53
|
...options,
|
|
37
54
|
};
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!Joi.isSchema(schema)) {
|
|
44
|
-
throw new Error("The schema object must be of type Joi schema.");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!schema.describe().keys.username || !schema.describe().keys.password) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
"The schema object must have a username and password defined."
|
|
50
|
-
);
|
|
51
|
-
}
|
|
56
|
+
checkUserDao(dao);
|
|
57
|
+
checkSchemas(schemas);
|
|
52
58
|
|
|
53
59
|
const app = createBaseAppServer({ port, webpack, start });
|
|
54
60
|
|
|
@@ -56,21 +62,38 @@ export default function createAppServer(options) {
|
|
|
56
62
|
return app;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
if (!process.env.COOKIE_SECRET) {
|
|
66
|
+
console.warn("You need to specify a cookie secret!");
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
if (!process.env.JWT_SECRET) {
|
|
60
70
|
console.warn("You need to specify a JWT secret!");
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
// These secrets will not be stable between restarts
|
|
74
|
+
const cookieSecret = process.env.COOKIE_SECRET || uuid();
|
|
75
|
+
const jwtSecret = process.env.JWT_SECRET || uuid();
|
|
76
|
+
|
|
77
|
+
passport.use(
|
|
78
|
+
new LocalStrategy((username, password, done) => {
|
|
79
|
+
dao.getUserByUsernameAndPassword(username, password).then((user) => {
|
|
80
|
+
if (!user) {
|
|
81
|
+
return done(null, false);
|
|
82
|
+
}
|
|
83
|
+
return done(null, user);
|
|
84
|
+
});
|
|
85
|
+
})
|
|
86
|
+
);
|
|
64
87
|
|
|
65
88
|
passport.use(
|
|
66
89
|
"jwt",
|
|
67
90
|
new JwtStrategy(
|
|
68
91
|
{
|
|
69
92
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
70
|
-
secretOrKey:
|
|
93
|
+
secretOrKey: jwtSecret,
|
|
71
94
|
// NOTE: Setting options like 'issuer' here must also be set when the token is signed below
|
|
72
95
|
jsonWebTokenOptions: {
|
|
73
|
-
expiresIn:
|
|
96
|
+
expiresIn: `${jwtExpireInMinutes}m`,
|
|
74
97
|
},
|
|
75
98
|
},
|
|
76
99
|
(payload, done) => {
|
|
@@ -78,27 +101,40 @@ export default function createAppServer(options) {
|
|
|
78
101
|
}
|
|
79
102
|
)
|
|
80
103
|
);
|
|
81
|
-
|
|
82
|
-
|
|
104
|
+
|
|
105
|
+
passport.serializeUser((user, done) => {
|
|
106
|
+
done(null, user);
|
|
83
107
|
});
|
|
84
|
-
|
|
108
|
+
|
|
109
|
+
passport.deserializeUser(({ id }, done) => {
|
|
85
110
|
dao
|
|
86
111
|
.getUserById(id)
|
|
87
112
|
.then((user) => {
|
|
88
113
|
// An undefined user means we couldn't find it, so the session is invalid
|
|
89
|
-
done(null, user
|
|
114
|
+
done(null, !user ? false : user);
|
|
90
115
|
})
|
|
91
116
|
.catch((error) => {
|
|
92
117
|
done(error, null);
|
|
93
118
|
});
|
|
94
119
|
});
|
|
95
|
-
|
|
120
|
+
|
|
121
|
+
app.use(
|
|
122
|
+
expressSession({
|
|
123
|
+
secret: cookieSecret,
|
|
124
|
+
resave: true,
|
|
125
|
+
saveUninitialized: true,
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
app.use(passport.initialize());
|
|
129
|
+
app.use(passport.session());
|
|
96
130
|
|
|
97
131
|
app.use(
|
|
98
132
|
auth({
|
|
99
|
-
secret,
|
|
100
|
-
|
|
133
|
+
secret: jwtSecret,
|
|
134
|
+
schemas,
|
|
101
135
|
dao,
|
|
136
|
+
eventEmitter,
|
|
137
|
+
jwtExpireInMinutes,
|
|
102
138
|
})
|
|
103
139
|
);
|
|
104
140
|
|
|
@@ -106,5 +142,11 @@ export default function createAppServer(options) {
|
|
|
106
142
|
app.use(path, checkAuth());
|
|
107
143
|
});
|
|
108
144
|
|
|
145
|
+
// Handling 500
|
|
146
|
+
app.use(function (error, req, res, next) {
|
|
147
|
+
console.error(error);
|
|
148
|
+
res.status(500).json({ error: "Internal Error" });
|
|
149
|
+
});
|
|
150
|
+
|
|
109
151
|
return app;
|
|
110
152
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import UserDao from "./user-dao.js";
|
|
4
|
+
import knex from "knex";
|
|
5
|
+
|
|
6
|
+
export async function createBetterSqlite3Db() {
|
|
7
|
+
const db = knex({
|
|
8
|
+
client: "better-sqlite3",
|
|
9
|
+
connection: {
|
|
10
|
+
filename: ":memory:",
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
await db.schema.createTable("users", (table) => {
|
|
15
|
+
table.string("id").primary(); // UUID
|
|
16
|
+
table.string("username").notNullable();
|
|
17
|
+
table.string("password").notNullable();
|
|
18
|
+
table.string("email").nullable();
|
|
19
|
+
table.string("name").nullable();
|
|
20
|
+
table.string("verification_token").notNullable();
|
|
21
|
+
table.dateTime("verified_date").nullable();
|
|
22
|
+
table.dateTime("last_login").nullable();
|
|
23
|
+
table.dateTime("created_at").notNullable();
|
|
24
|
+
table.dateTime("last_updated").notNullable();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return db;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default class KnexUserDao extends UserDao {
|
|
31
|
+
#db;
|
|
32
|
+
|
|
33
|
+
constructor(db) {
|
|
34
|
+
super();
|
|
35
|
+
|
|
36
|
+
// TODO: Check if db is a knex instance and throw an error if it is now
|
|
37
|
+
this.#db = db;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @inheritdoc */
|
|
41
|
+
async getUserById(userId) {
|
|
42
|
+
return await this.#db("users").select().where({ id: userId }).first();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @inheritdoc */
|
|
46
|
+
async getUserByUsername(username) {
|
|
47
|
+
const user = await this.#db
|
|
48
|
+
.select()
|
|
49
|
+
.from("users")
|
|
50
|
+
.where({ username: username })
|
|
51
|
+
.first();
|
|
52
|
+
|
|
53
|
+
return !user ? false : user;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @inheritdoc */
|
|
57
|
+
async getUserByUsernameAndPassword(username, password) {
|
|
58
|
+
// Treat this like a failed login.
|
|
59
|
+
if (!username || !password) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const user = await this.getUserByUsername(username);
|
|
64
|
+
|
|
65
|
+
if (!user || !bcrypt.compareSync(password, user.password)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return user;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @inheritdoc */
|
|
73
|
+
async getUserByVerificationToken(token) {
|
|
74
|
+
if (!token) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const user = await this.#db("users")
|
|
79
|
+
.select()
|
|
80
|
+
.where({ verification_token: token })
|
|
81
|
+
.first();
|
|
82
|
+
|
|
83
|
+
return !user ? false : user;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @inheritdoc */
|
|
87
|
+
async createUser(user) {
|
|
88
|
+
const existing = await this.getUserByUsername(user.username);
|
|
89
|
+
|
|
90
|
+
if (existing) {
|
|
91
|
+
throw new Error("This username is already taken.");
|
|
92
|
+
}
|
|
93
|
+
const now = new Date();
|
|
94
|
+
|
|
95
|
+
const data = {
|
|
96
|
+
...user,
|
|
97
|
+
password: bcrypt.hashSync(user.password, 10),
|
|
98
|
+
id: uuidv4(),
|
|
99
|
+
verification_token: uuidv4(),
|
|
100
|
+
created_at: now,
|
|
101
|
+
last_updated: now,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await this.#db("users").insert(data);
|
|
105
|
+
|
|
106
|
+
return await this.getUserById(data.id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @inheritdoc */
|
|
110
|
+
async updateUser(userId, user) {
|
|
111
|
+
if (!userId) {
|
|
112
|
+
throw new Error("User ID is required.");
|
|
113
|
+
}
|
|
114
|
+
if (!user) {
|
|
115
|
+
throw new Error("User data is required");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const existing = await this.getUserById(userId);
|
|
119
|
+
|
|
120
|
+
if (!existing) {
|
|
121
|
+
throw new Error("User does not exist.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await this.#db("users").where({ id: userId }).update(user);
|
|
125
|
+
|
|
126
|
+
return await this.getUserById(userId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @inheritdoc */
|
|
130
|
+
async deleteUser(userId) {
|
|
131
|
+
return !!(await this.#db("users").where({ id: userId }).delete());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getAllUsers() {
|
|
135
|
+
const users = await this.#db("users").select();
|
|
136
|
+
return users;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
close() {
|
|
140
|
+
this.#db.destroy();
|
|
141
|
+
}
|
|
142
|
+
}
|