@stanlemon/server-with-auth 0.1.30 → 0.2.1
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/app.js +12 -8
- package/package.json +4 -5
- package/src/createAppServer.js +36 -30
- package/src/data/lowdb-user-dao.js +137 -0
- package/src/data/{simple-users-dao.test.js → lowdb-user-dao.test.js} +34 -18
- package/src/data/simple-users-dao.js +5 -115
- package/src/data/user-dao.js +73 -0
- package/src/index.js +6 -0
- package/src/routes/auth.js +13 -17
- package/src/routes/auth.test.js +6 -7
package/app.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { createAppServer, asyncJsonHandler as handler } from "./src/index.js";
|
|
2
|
-
import
|
|
2
|
+
import LowDBUserDao, { createDb } from "./src/data/lowdb-user-dao.js";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const db = createDb();
|
|
5
|
+
const dao = new LowDBUserDao(db);
|
|
5
6
|
|
|
6
7
|
const app = createAppServer({
|
|
7
8
|
port: 3003,
|
|
8
9
|
secure: ["/api/"],
|
|
9
|
-
|
|
10
|
+
dao,
|
|
10
11
|
});
|
|
11
12
|
|
|
13
|
+
// Insecure endpoint
|
|
12
14
|
app.get(
|
|
13
15
|
"/",
|
|
14
|
-
handler((
|
|
16
|
+
handler(() => ({ hello: "world" }))
|
|
15
17
|
);
|
|
16
18
|
|
|
19
|
+
// Insecure endpoint
|
|
17
20
|
app.get(
|
|
18
|
-
"/
|
|
19
|
-
handler(() => ({
|
|
21
|
+
"/hello/:name",
|
|
22
|
+
handler(({ name = "world" }) => ({ hello: name }))
|
|
20
23
|
);
|
|
21
24
|
|
|
25
|
+
// Secure endpoint
|
|
22
26
|
app.get(
|
|
23
|
-
"/
|
|
24
|
-
handler(() => ({
|
|
27
|
+
"/api/users",
|
|
28
|
+
handler(() => ({ users: db.data.users }))
|
|
25
29
|
);
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stanlemon/server-with-auth",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=18.0"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "./src/index.js",
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"lowdb-node": "^3.0.2",
|
|
28
28
|
"passport": "^0.6.0",
|
|
29
29
|
"passport-jwt": "^4.0.1",
|
|
30
|
-
"
|
|
31
|
-
"uuid": "^9.0.0"
|
|
30
|
+
"uuid": "^9.0.1"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
33
|
"@stanlemon/eslint-config": "*",
|
|
@@ -36,4 +35,4 @@
|
|
|
36
35
|
"nodemon": "^3.0.1",
|
|
37
36
|
"supertest": "^6.3.3"
|
|
38
37
|
}
|
|
39
|
-
}
|
|
38
|
+
}
|
package/src/createAppServer.js
CHANGED
|
@@ -5,40 +5,50 @@ import {
|
|
|
5
5
|
} from "@stanlemon/server";
|
|
6
6
|
import passport from "passport";
|
|
7
7
|
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
|
|
8
|
-
import
|
|
8
|
+
import { v4 as uuid } from "uuid";
|
|
9
|
+
import Joi from "joi";
|
|
9
10
|
import defaultUserSchema from "./schema/user.js";
|
|
10
11
|
import checkAuth from "./checkAuth.js";
|
|
11
12
|
import auth from "./routes/auth.js";
|
|
13
|
+
import UserDao from "./data/user-dao.js";
|
|
12
14
|
|
|
13
15
|
dotenv.config();
|
|
14
16
|
|
|
15
|
-
// TODO: Add option for schema
|
|
16
17
|
export const DEFAULTS = {
|
|
17
18
|
...BASE_DEFAULTS,
|
|
18
19
|
secure: [],
|
|
19
20
|
schema: defaultUserSchema,
|
|
20
|
-
|
|
21
|
-
getUserByUsername: (username) => {},
|
|
22
|
-
getUserByUsernameAndPassword: (username, password) => {},
|
|
23
|
-
getUserByVerificationToken: (token) => {},
|
|
24
|
-
createUser: (user) => {},
|
|
25
|
-
updateUser: (userId, user) => {},
|
|
21
|
+
dao: new UserDao(),
|
|
26
22
|
};
|
|
27
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Create an app server with authentication.
|
|
26
|
+
* @param {number} options.port Port to listen on
|
|
27
|
+
* @param {boolean} options.webpack Whether or not to create a proxy for webpack
|
|
28
|
+
* @param {string[]} options.secure Paths that require authentication
|
|
29
|
+
* @param {Joi.Schema} options.schema Joi schema for user object
|
|
30
|
+
* @param {UserDao} options.dao Data access object for user interactions
|
|
31
|
+
* @returns {import("express").Express} Express app
|
|
32
|
+
*/
|
|
28
33
|
export default function createAppServer(options) {
|
|
29
|
-
const {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
const { port, webpack, start, secure, schema, dao } = {
|
|
35
|
+
...DEFAULTS,
|
|
36
|
+
...options,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (!(dao instanceof UserDao)) {
|
|
40
|
+
throw new Error("The dao object must be of type UserDao.");
|
|
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
|
+
}
|
|
42
52
|
|
|
43
53
|
const app = createBaseAppServer({ port, webpack, start });
|
|
44
54
|
|
|
@@ -47,10 +57,10 @@ export default function createAppServer(options) {
|
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
if (!process.env.JWT_SECRET) {
|
|
50
|
-
console.warn("You need to specify a secret
|
|
60
|
+
console.warn("You need to specify a JWT secret!");
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
const secret = process.env.JWT_SECRET ||
|
|
63
|
+
const secret = process.env.JWT_SECRET || uuid();
|
|
54
64
|
|
|
55
65
|
passport.use(
|
|
56
66
|
"jwt",
|
|
@@ -72,7 +82,8 @@ export default function createAppServer(options) {
|
|
|
72
82
|
done(null, id);
|
|
73
83
|
});
|
|
74
84
|
passport.deserializeUser((id, done) => {
|
|
75
|
-
|
|
85
|
+
dao
|
|
86
|
+
.getUserById(id)
|
|
76
87
|
.then((user) => {
|
|
77
88
|
// An undefined user means we couldn't find it, so the session is invalid
|
|
78
89
|
done(null, user === undefined ? false : user);
|
|
@@ -87,12 +98,7 @@ export default function createAppServer(options) {
|
|
|
87
98
|
auth({
|
|
88
99
|
secret,
|
|
89
100
|
schema,
|
|
90
|
-
|
|
91
|
-
getUserByUsername,
|
|
92
|
-
getUserByUsernameAndPassword,
|
|
93
|
-
getUserByVerificationToken,
|
|
94
|
-
createUser,
|
|
95
|
-
updateUser,
|
|
101
|
+
dao,
|
|
96
102
|
})
|
|
97
103
|
);
|
|
98
104
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { LowSync, MemorySync } from "lowdb";
|
|
2
|
+
import { JSONFileSync } from "lowdb-node";
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
import bcrypt from "bcryptjs";
|
|
5
|
+
import UserDao from "./user-dao.js";
|
|
6
|
+
|
|
7
|
+
export function createInMemoryDb() {
|
|
8
|
+
return new LowSync(new MemorySync(), {});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createJsonFileDb(filename = "./db.json") {
|
|
12
|
+
return new LowSync(new JSONFileSync(filename), {});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a database based on the environment.
|
|
17
|
+
* Test environment (NODE_ENV=test) will use {MemorySync}.
|
|
18
|
+
* @returns {LowSync} Database
|
|
19
|
+
*/
|
|
20
|
+
export function createDb() {
|
|
21
|
+
return process.env.NODE_ENV === "test"
|
|
22
|
+
? createInMemoryDb()
|
|
23
|
+
: createJsonFileDb();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default class LowDBUserDao extends UserDao {
|
|
27
|
+
#db;
|
|
28
|
+
|
|
29
|
+
constructor(db = createDb()) {
|
|
30
|
+
super();
|
|
31
|
+
|
|
32
|
+
if (!(db instanceof LowSync)) {
|
|
33
|
+
throw new Error("The db object must be of type LowSync.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.#db = db;
|
|
37
|
+
this.#db.read();
|
|
38
|
+
|
|
39
|
+
// Default data, ensure that users is an array if it is not already
|
|
40
|
+
this.#db.data.users = db.data.users || [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @inheritdoc */
|
|
44
|
+
getUserById(userId) {
|
|
45
|
+
return this.#db.data.users
|
|
46
|
+
.filter((user) => {
|
|
47
|
+
// This one can get gross with numerical ids
|
|
48
|
+
// eslint-disable-next-line eqeqeq
|
|
49
|
+
return user.id == userId;
|
|
50
|
+
})
|
|
51
|
+
.shift();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @inheritdoc */
|
|
55
|
+
getUserByUsername(username) {
|
|
56
|
+
return this.#db.data.users
|
|
57
|
+
.filter((user) => {
|
|
58
|
+
return user.username === username;
|
|
59
|
+
})
|
|
60
|
+
.shift();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @inheritdoc */
|
|
64
|
+
getUserByUsernameAndPassword(username, password) {
|
|
65
|
+
// Treat this like a failed login.
|
|
66
|
+
if (!username || !password) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const user = this.getUserByUsername(username);
|
|
71
|
+
|
|
72
|
+
if (!user || !bcrypt.compareSync(password, user.password)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return user;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @inheritdoc */
|
|
80
|
+
getUserByVerificationToken(token) {
|
|
81
|
+
return this.#db.data.users
|
|
82
|
+
.filter((user) => user.verification_token === token)
|
|
83
|
+
.shift();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @inheritdoc */
|
|
87
|
+
createUser(user) {
|
|
88
|
+
const existing = this.getUserByUsername(user.username);
|
|
89
|
+
|
|
90
|
+
if (existing) {
|
|
91
|
+
throw new Error("This username is already taken.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const now = new Date();
|
|
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
|
+
this.#db.data.users.push(data);
|
|
104
|
+
this.#db.write();
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @inheritdoc */
|
|
109
|
+
updateUser(userId, user) {
|
|
110
|
+
const now = new Date();
|
|
111
|
+
this.#db.data.users = this.#db.data.users.map((u) => {
|
|
112
|
+
if (u.id === userId) {
|
|
113
|
+
// If the password has been set, encrypt it
|
|
114
|
+
if (user.password) {
|
|
115
|
+
user.password = bcrypt.hashSync(user.password, 10);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
...u,
|
|
120
|
+
...user,
|
|
121
|
+
id: userId,
|
|
122
|
+
last_updated: now,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return u;
|
|
126
|
+
});
|
|
127
|
+
this.#db.write();
|
|
128
|
+
return this.getUserById(userId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @inheritdoc */
|
|
132
|
+
deleteUser(userId) {
|
|
133
|
+
this.#db.data.users = this.#db.data.users.filter((u) => u.id !== userId);
|
|
134
|
+
this.#db.write();
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @jest-environment node
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import SimpleUsersDao from "./simple-users-dao.js";
|
|
4
|
+
import LowDBUserDao, { createInMemoryDb } from "./lowdb-user-dao.js";
|
|
6
5
|
|
|
7
|
-
describe("
|
|
6
|
+
describe("lowdb-user-dao", () => {
|
|
8
7
|
// This is a user that we will reuse in our tests
|
|
9
8
|
const data = {
|
|
10
9
|
username: "test",
|
|
11
10
|
password: "password",
|
|
12
11
|
};
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
|
|
13
|
+
/** @type {LowSync} */
|
|
14
|
+
let db;
|
|
15
|
+
/** @type {LowDBUserDao} */
|
|
16
|
+
let dao;
|
|
15
17
|
|
|
16
18
|
beforeEach(() => {
|
|
17
19
|
// Before each test reset our users database
|
|
18
|
-
|
|
20
|
+
db = createInMemoryDb();
|
|
21
|
+
dao = new LowDBUserDao(db);
|
|
19
22
|
});
|
|
20
23
|
|
|
21
24
|
it("creates a user", async () => {
|
|
22
|
-
let user = await
|
|
25
|
+
let user = await dao.createUser(data);
|
|
23
26
|
|
|
24
27
|
expect(user.username).toEqual(data.username);
|
|
25
28
|
// The value should be encrypted now
|
|
@@ -29,7 +32,7 @@ describe("simple-users-dao", () => {
|
|
|
29
32
|
expect(user.created_at).not.toBeUndefined();
|
|
30
33
|
expect(user.last_updated).not.toBeUndefined();
|
|
31
34
|
|
|
32
|
-
const refresh =
|
|
35
|
+
const refresh = dao.getUserByUsername(data.username);
|
|
33
36
|
|
|
34
37
|
// If we retrieve the user by username, it matches the object we got when we created it
|
|
35
38
|
expect(refresh).toMatchObject(user);
|
|
@@ -40,9 +43,9 @@ describe("simple-users-dao", () => {
|
|
|
40
43
|
|
|
41
44
|
try {
|
|
42
45
|
// Create the user
|
|
43
|
-
await
|
|
46
|
+
await dao.createUser(data);
|
|
44
47
|
// Attempt to create the user again, this will fail
|
|
45
|
-
await
|
|
48
|
+
await dao.createUser(data);
|
|
46
49
|
} catch (err) {
|
|
47
50
|
hasError = err;
|
|
48
51
|
}
|
|
@@ -52,9 +55,9 @@ describe("simple-users-dao", () => {
|
|
|
52
55
|
});
|
|
53
56
|
|
|
54
57
|
it("retrieve user by username and password", async () => {
|
|
55
|
-
const user1 = await
|
|
58
|
+
const user1 = await dao.createUser(data);
|
|
56
59
|
|
|
57
|
-
const user2 =
|
|
60
|
+
const user2 = dao.getUserByUsernameAndPassword(
|
|
58
61
|
data.username,
|
|
59
62
|
data.password
|
|
60
63
|
);
|
|
@@ -63,9 +66,9 @@ describe("simple-users-dao", () => {
|
|
|
63
66
|
});
|
|
64
67
|
|
|
65
68
|
it("retrieve user by username and wrong password fails", async () => {
|
|
66
|
-
await
|
|
69
|
+
await dao.createUser(data);
|
|
67
70
|
|
|
68
|
-
const user2 =
|
|
71
|
+
const user2 = dao.getUserByUsernameAndPassword(
|
|
69
72
|
data.username,
|
|
70
73
|
"wrong password"
|
|
71
74
|
);
|
|
@@ -74,22 +77,35 @@ describe("simple-users-dao", () => {
|
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
it("retrieve user by username and undefined password fails", async () => {
|
|
77
|
-
await
|
|
80
|
+
await dao.createUser(data);
|
|
78
81
|
|
|
79
|
-
const user2 =
|
|
82
|
+
const user2 = dao.getUserByUsernameAndPassword(data.username, undefined);
|
|
80
83
|
|
|
81
84
|
expect(user2).toBe(false);
|
|
82
85
|
});
|
|
83
86
|
|
|
84
87
|
it("retrieve user by username that does not exist fails", async () => {
|
|
85
|
-
const user =
|
|
88
|
+
const user = dao.getUserByUsernameAndPassword("notarealuser", "password");
|
|
86
89
|
|
|
87
90
|
expect(user).toBe(false);
|
|
88
91
|
});
|
|
89
92
|
|
|
90
93
|
it("retrieve user by username that is undefined fails", async () => {
|
|
91
|
-
const user =
|
|
94
|
+
const user = dao.getUserByUsernameAndPassword(undefined, "password");
|
|
92
95
|
|
|
93
96
|
expect(user).toBe(false);
|
|
94
97
|
});
|
|
98
|
+
|
|
99
|
+
it("deletes a user by id", async () => {
|
|
100
|
+
const user = await dao.createUser(data);
|
|
101
|
+
|
|
102
|
+
expect(db.data.users).toHaveLength(1);
|
|
103
|
+
|
|
104
|
+
const deleted = dao.deleteUser(user.id);
|
|
105
|
+
|
|
106
|
+
expect(deleted).toBe(true);
|
|
107
|
+
expect(dao.getUserById(user.id)).toBeUndefined();
|
|
108
|
+
|
|
109
|
+
expect(db.data.users).toHaveLength(0);
|
|
110
|
+
});
|
|
95
111
|
});
|
|
@@ -1,116 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { JSONFileSync } from "lowdb-node";
|
|
3
|
-
import { v4 as uuidv4 } from "uuid";
|
|
4
|
-
import shortid from "shortid";
|
|
5
|
-
import bcrypt from "bcryptjs";
|
|
1
|
+
import LowDBUserDao from "./lowdb-user-dao.js";
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export default class SimpleUsersDao {
|
|
13
|
-
constructor(seeds = [], adapter = DEFAULT_ADAPTER) {
|
|
14
|
-
this.db = new LowSync(adapter, { users: [] });
|
|
15
|
-
|
|
16
|
-
this.db.read();
|
|
17
|
-
|
|
18
|
-
if (seeds.length > 0) {
|
|
19
|
-
seeds.forEach((user) => this.createUser(user));
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
getDb() {
|
|
24
|
-
return this.db;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
generateId() {
|
|
28
|
-
return uuidv4();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
generateVerificationToken() {
|
|
32
|
-
return shortid.generate();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
getUserById = (userId) => {
|
|
36
|
-
return this.db.data.users
|
|
37
|
-
.filter((user) => {
|
|
38
|
-
// This one can get gross with numerical ids
|
|
39
|
-
// eslint-disable-next-line eqeqeq
|
|
40
|
-
return user.id == userId;
|
|
41
|
-
})
|
|
42
|
-
.shift();
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
getUserByUsername = (username) => {
|
|
46
|
-
return this.db.data.users
|
|
47
|
-
.filter((user) => {
|
|
48
|
-
return user.username === username;
|
|
49
|
-
})
|
|
50
|
-
.shift();
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
getUserByUsernameAndPassword = (username, password) => {
|
|
54
|
-
// Treat this like a failed login.
|
|
55
|
-
if (!username || !password) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const user = this.getUserByUsername(username);
|
|
60
|
-
|
|
61
|
-
if (!user || !bcrypt.compareSync(password, user.password)) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return user;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
getUserByVerificationToken = (token) => {
|
|
69
|
-
return this.db.data.users
|
|
70
|
-
.filter((user) => user.verification_token === token)
|
|
71
|
-
.shift();
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
createUser = (user) => {
|
|
75
|
-
const existing = this.getUserByUsername(user.username);
|
|
76
|
-
|
|
77
|
-
if (existing) {
|
|
78
|
-
throw new Error("This username is already taken.");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const now = new Date();
|
|
82
|
-
const data = {
|
|
83
|
-
...user,
|
|
84
|
-
password: bcrypt.hashSync(user.password, 10),
|
|
85
|
-
id: this.generateId(),
|
|
86
|
-
verification_token: this.generateVerificationToken(),
|
|
87
|
-
created_at: now,
|
|
88
|
-
last_updated: now,
|
|
89
|
-
};
|
|
90
|
-
this.db.data.users.push(data);
|
|
91
|
-
this.db.write();
|
|
92
|
-
return data;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
updateUser = (userId, user) => {
|
|
96
|
-
const now = new Date();
|
|
97
|
-
this.db.data.users = this.db.data.users.map((u) => {
|
|
98
|
-
if (u.id === userId) {
|
|
99
|
-
// If the password has been set, encrypt it
|
|
100
|
-
if (user.password) {
|
|
101
|
-
user.password = bcrypt.hashSync(user.password, 10);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
...u,
|
|
106
|
-
...user,
|
|
107
|
-
id: userId,
|
|
108
|
-
last_updated: now,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return u;
|
|
112
|
-
});
|
|
113
|
-
this.db.write();
|
|
114
|
-
return this.getUserById(userId);
|
|
115
|
-
};
|
|
116
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated since version 2.0, use LowDBUserDao instead.
|
|
5
|
+
*/
|
|
6
|
+
export default class SimpleUsersDao extends LowDBUserDao {}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export default class UserDao {
|
|
2
|
+
constructor() {}
|
|
3
|
+
|
|
4
|
+
#error() {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"The UserDao class serves as an interface, do not use it directly."
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get user by id.
|
|
12
|
+
* @param {*} userId Identifier to get user by
|
|
13
|
+
*/
|
|
14
|
+
// eslint-disable-next-line no-unused-vars
|
|
15
|
+
getUserById(userId) {
|
|
16
|
+
this.#error();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get user by username.
|
|
21
|
+
* @param {string} username Username
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line no-unused-vars
|
|
24
|
+
getUserByUsername(username) {
|
|
25
|
+
this.#error();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get user by username and password.
|
|
30
|
+
* @param {string} username Username
|
|
31
|
+
* @param {string} password Password
|
|
32
|
+
*/
|
|
33
|
+
// eslint-disable-next-line no-unused-vars
|
|
34
|
+
getUserByUsernameAndPassword(username, password) {
|
|
35
|
+
this.#error();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get user by verification token.
|
|
40
|
+
* @param {string} token User verification token
|
|
41
|
+
*/
|
|
42
|
+
// eslint-disable-next-line no-unused-vars
|
|
43
|
+
getUserByVerificationToken(token) {
|
|
44
|
+
this.#error();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a new user.
|
|
49
|
+
* @param {object} user User
|
|
50
|
+
*/
|
|
51
|
+
// eslint-disable-next-line no-unused-vars
|
|
52
|
+
createUser(user) {
|
|
53
|
+
this.#error();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Update an existing user.
|
|
58
|
+
* @param {*} userId User identifier
|
|
59
|
+
* @param {object} user User object
|
|
60
|
+
*/
|
|
61
|
+
// eslint-disable-next-line no-unused-vars
|
|
62
|
+
updateUser(userId, user) {
|
|
63
|
+
this.#error();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Delete a user.
|
|
68
|
+
* @param {*} userId User identifier
|
|
69
|
+
*/
|
|
70
|
+
deleteUser(userId) {
|
|
71
|
+
this.#error();
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/index.js
CHANGED
|
@@ -13,3 +13,9 @@ export { default as checkAuth } from "./checkAuth.js";
|
|
|
13
13
|
export { default as createAppServer } from "./createAppServer.js";
|
|
14
14
|
export { default as schema } from "./schema/user.js";
|
|
15
15
|
export { default as SimpleUsersDao } from "./data/simple-users-dao.js";
|
|
16
|
+
export {
|
|
17
|
+
default as LowDBUserDao,
|
|
18
|
+
createDb,
|
|
19
|
+
createInMemoryDb,
|
|
20
|
+
createJsonFileDb,
|
|
21
|
+
} from "./data/lowdb-user-dao.js";
|
package/src/routes/auth.js
CHANGED
|
@@ -7,18 +7,14 @@ import {
|
|
|
7
7
|
BadRequestException,
|
|
8
8
|
} from "@stanlemon/server";
|
|
9
9
|
import checkAuth from "../checkAuth.js";
|
|
10
|
+
import UserDao from "../data/user-dao.js";
|
|
10
11
|
|
|
11
12
|
/* eslint-disable max-lines-per-function */
|
|
12
|
-
export default function authRoutes({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
getUserByUsernameAndPassword,
|
|
18
|
-
getUserByVerificationToken,
|
|
19
|
-
createUser,
|
|
20
|
-
updateUser,
|
|
21
|
-
}) {
|
|
13
|
+
export default function authRoutes({ secret, schema, dao }) {
|
|
14
|
+
if (!(dao instanceof UserDao)) {
|
|
15
|
+
throw new Error("The dao object must be of type UserDao.");
|
|
16
|
+
}
|
|
17
|
+
|
|
22
18
|
const router = Router();
|
|
23
19
|
|
|
24
20
|
router.get("/auth/session", checkAuth(), async (req, res) => {
|
|
@@ -32,7 +28,7 @@ export default function authRoutes({
|
|
|
32
28
|
return;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
const user = await getUserById(userId);
|
|
31
|
+
const user = await dao.getUserById(userId);
|
|
36
32
|
|
|
37
33
|
if (!user) {
|
|
38
34
|
res.status(401).json({
|
|
@@ -46,7 +42,7 @@ export default function authRoutes({
|
|
|
46
42
|
});
|
|
47
43
|
|
|
48
44
|
router.post("/auth/login", async (req, res) => {
|
|
49
|
-
const user = await getUserByUsernameAndPassword(
|
|
45
|
+
const user = await dao.getUserByUsernameAndPassword(
|
|
50
46
|
req.body.username,
|
|
51
47
|
req.body.password
|
|
52
48
|
);
|
|
@@ -58,7 +54,7 @@ export default function authRoutes({
|
|
|
58
54
|
return;
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
const update = await updateUser(user.id, {
|
|
57
|
+
const update = await dao.updateUser(user.id, {
|
|
62
58
|
last_logged_in: new Date(),
|
|
63
59
|
});
|
|
64
60
|
|
|
@@ -82,7 +78,7 @@ export default function authRoutes({
|
|
|
82
78
|
router.post(
|
|
83
79
|
"/auth/register",
|
|
84
80
|
schemaHandler(schema, async (data) => {
|
|
85
|
-
const existing = await getUserByUsername(data.username);
|
|
81
|
+
const existing = await dao.getUserByUsername(data.username);
|
|
86
82
|
|
|
87
83
|
if (existing) {
|
|
88
84
|
throw new BadRequestException(
|
|
@@ -90,7 +86,7 @@ export default function authRoutes({
|
|
|
90
86
|
);
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
const user = await createUser(data);
|
|
89
|
+
const user = await dao.createUser(data);
|
|
94
90
|
|
|
95
91
|
if (isEmpty(user)) {
|
|
96
92
|
return {
|
|
@@ -109,7 +105,7 @@ export default function authRoutes({
|
|
|
109
105
|
router.get("/auth/verify/:token", async (req, res) => {
|
|
110
106
|
const { token } = req.params;
|
|
111
107
|
|
|
112
|
-
const user = await getUserByVerificationToken(token);
|
|
108
|
+
const user = await dao.getUserByVerificationToken(token);
|
|
113
109
|
|
|
114
110
|
if (isEmpty(user)) {
|
|
115
111
|
return res
|
|
@@ -123,7 +119,7 @@ export default function authRoutes({
|
|
|
123
119
|
.send({ success: false, message: "User already verified." });
|
|
124
120
|
}
|
|
125
121
|
|
|
126
|
-
await updateUser(user.id, { verified_date: new Date() });
|
|
122
|
+
await dao.updateUser(user.id, { verified_date: new Date() });
|
|
127
123
|
|
|
128
124
|
return res.send({ success: true, message: "User verified!" });
|
|
129
125
|
});
|
package/src/routes/auth.test.js
CHANGED
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
* @jest-environment node
|
|
3
3
|
*/
|
|
4
4
|
import request from "supertest";
|
|
5
|
-
import { MemorySync } from "lowdb";
|
|
6
5
|
import createAppServer from "../createAppServer";
|
|
7
|
-
import
|
|
6
|
+
import LowDBUserDao, { createInMemoryDb } from "../data/lowdb-user-dao.js";
|
|
8
7
|
|
|
9
8
|
// This suppresses a warning we don't need in tests
|
|
10
9
|
process.env.JWT_SECRET = "SECRET";
|
|
11
10
|
|
|
12
|
-
let
|
|
11
|
+
let dao = new LowDBUserDao(createInMemoryDb());
|
|
13
12
|
|
|
14
13
|
// We want to explicitly test functionality we disable during testing
|
|
15
14
|
process.env.NODE_ENV = "override";
|
|
16
|
-
const app = createAppServer({
|
|
15
|
+
const app = createAppServer({ dao, start: false });
|
|
17
16
|
process.env.NODE_ENV = "test";
|
|
18
17
|
|
|
19
18
|
describe("/auth", () => {
|
|
@@ -21,7 +20,7 @@ describe("/auth", () => {
|
|
|
21
20
|
|
|
22
21
|
beforeAll(async () => {
|
|
23
22
|
// Reset our users database before each test
|
|
24
|
-
const user = await
|
|
23
|
+
const user = await dao.createUser({
|
|
25
24
|
username: "test",
|
|
26
25
|
password: "test",
|
|
27
26
|
});
|
|
@@ -116,7 +115,7 @@ describe("/auth", () => {
|
|
|
116
115
|
});
|
|
117
116
|
|
|
118
117
|
it("GET /verify verifies user", async () => {
|
|
119
|
-
const user =
|
|
118
|
+
const user = dao.getUserById(userId);
|
|
120
119
|
|
|
121
120
|
expect(user.verification_token).not.toBe(null);
|
|
122
121
|
expect(user.verified_date).toBeUndefined();
|
|
@@ -131,7 +130,7 @@ describe("/auth", () => {
|
|
|
131
130
|
expect(res.body.success).toEqual(true);
|
|
132
131
|
});
|
|
133
132
|
|
|
134
|
-
const refresh = await
|
|
133
|
+
const refresh = await dao.getUserById(userId);
|
|
135
134
|
|
|
136
135
|
expect(refresh.verified_date).not.toBe(null);
|
|
137
136
|
|