@stanlemon/server-with-auth 0.1.30 → 0.2.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/app.js +11 -8
- package/package.json +4 -5
- package/src/createAppServer.js +36 -30
- package/src/data/lowdb-user-dao.js +124 -0
- package/src/data/{simple-users-dao.test.js → lowdb-user-dao.test.js} +31 -17
- package/src/data/simple-users-dao.js +5 -115
- package/src/data/user-dao.js +73 -0
- package/src/index.js +1 -0
- package/src/routes/auth.js +13 -17
- package/src/routes/auth.test.js +6 -6
package/app.js
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import { createAppServer, asyncJsonHandler as handler } from "./src/index.js";
|
|
2
|
-
import
|
|
2
|
+
import LowDBUserDao from "./src/data/lowdb-user-dao.js";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const dao = new LowDBUserDao();
|
|
5
5
|
|
|
6
6
|
const app = createAppServer({
|
|
7
7
|
port: 3003,
|
|
8
8
|
secure: ["/api/"],
|
|
9
|
-
|
|
9
|
+
dao,
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
// Insecure endpoint
|
|
12
13
|
app.get(
|
|
13
14
|
"/",
|
|
14
|
-
handler((
|
|
15
|
+
handler(() => ({ hello: "world" }))
|
|
15
16
|
);
|
|
16
17
|
|
|
18
|
+
// Insecure endpoint
|
|
17
19
|
app.get(
|
|
18
|
-
"/
|
|
19
|
-
handler(() => ({
|
|
20
|
+
"/hello/:name",
|
|
21
|
+
handler(({ name = "world" }) => ({ hello: name }))
|
|
20
22
|
);
|
|
21
23
|
|
|
24
|
+
// Secure endpoint
|
|
22
25
|
app.get(
|
|
23
|
-
"/
|
|
24
|
-
handler(() => ({
|
|
26
|
+
"/api/users",
|
|
27
|
+
handler(() => ({ users: dao.getDB().data.users }))
|
|
25
28
|
);
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stanlemon/server-with-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
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,124 @@
|
|
|
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
|
+
const DEFAULT_ADAPTER =
|
|
8
|
+
process.env.NODE_ENV === "test"
|
|
9
|
+
? new MemorySync()
|
|
10
|
+
: new JSONFileSync("./db.json");
|
|
11
|
+
|
|
12
|
+
export default class LowDBUserDao extends UserDao {
|
|
13
|
+
#db;
|
|
14
|
+
|
|
15
|
+
constructor(seeds = [], adapter = DEFAULT_ADAPTER) {
|
|
16
|
+
super();
|
|
17
|
+
|
|
18
|
+
this.#db = new LowSync(adapter, { users: [] });
|
|
19
|
+
this.#db.read();
|
|
20
|
+
|
|
21
|
+
if (seeds.length > 0) {
|
|
22
|
+
seeds.forEach((user) => this.createUser(user));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getDB() {
|
|
27
|
+
return this.#db;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @inheritdoc */
|
|
31
|
+
getUserById(userId) {
|
|
32
|
+
return this.#db.data.users
|
|
33
|
+
.filter((user) => {
|
|
34
|
+
// This one can get gross with numerical ids
|
|
35
|
+
// eslint-disable-next-line eqeqeq
|
|
36
|
+
return user.id == userId;
|
|
37
|
+
})
|
|
38
|
+
.shift();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @inheritdoc */
|
|
42
|
+
getUserByUsername(username) {
|
|
43
|
+
return this.#db.data.users
|
|
44
|
+
.filter((user) => {
|
|
45
|
+
return user.username === username;
|
|
46
|
+
})
|
|
47
|
+
.shift();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @inheritdoc */
|
|
51
|
+
getUserByUsernameAndPassword(username, password) {
|
|
52
|
+
// Treat this like a failed login.
|
|
53
|
+
if (!username || !password) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const user = this.getUserByUsername(username);
|
|
58
|
+
|
|
59
|
+
if (!user || !bcrypt.compareSync(password, user.password)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return user;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @inheritdoc */
|
|
67
|
+
getUserByVerificationToken(token) {
|
|
68
|
+
return this.#db.data.users
|
|
69
|
+
.filter((user) => user.verification_token === token)
|
|
70
|
+
.shift();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @inheritdoc */
|
|
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: uuidv4(),
|
|
86
|
+
verification_token: uuidv4(),
|
|
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
|
+
/** @inheritdoc */
|
|
96
|
+
updateUser(userId, user) {
|
|
97
|
+
const now = new Date();
|
|
98
|
+
this.#db.data.users = this.#db.data.users.map((u) => {
|
|
99
|
+
if (u.id === userId) {
|
|
100
|
+
// If the password has been set, encrypt it
|
|
101
|
+
if (user.password) {
|
|
102
|
+
user.password = bcrypt.hashSync(user.password, 10);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
...u,
|
|
107
|
+
...user,
|
|
108
|
+
id: userId,
|
|
109
|
+
last_updated: now,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return u;
|
|
113
|
+
});
|
|
114
|
+
this.#db.write();
|
|
115
|
+
return this.getUserById(userId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @inheritdoc */
|
|
119
|
+
deleteUser(userId) {
|
|
120
|
+
this.#db.data.users = this.#db.data.users.filter((u) => u.id !== userId);
|
|
121
|
+
this.#db.write();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -2,24 +2,25 @@
|
|
|
2
2
|
* @jest-environment node
|
|
3
3
|
*/
|
|
4
4
|
import { MemorySync } from "lowdb";
|
|
5
|
-
import
|
|
5
|
+
import LowDBUserDao from "./lowdb-user-dao.js";
|
|
6
6
|
|
|
7
|
-
describe("
|
|
7
|
+
describe("lowdb-user-dao", () => {
|
|
8
8
|
// This is a user that we will reuse in our tests
|
|
9
9
|
const data = {
|
|
10
10
|
username: "test",
|
|
11
11
|
password: "password",
|
|
12
12
|
};
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
/** @type {LowDBUserDao} */
|
|
15
|
+
let dao;
|
|
15
16
|
|
|
16
17
|
beforeEach(() => {
|
|
17
18
|
// Before each test reset our users database
|
|
18
|
-
|
|
19
|
+
dao = new LowDBUserDao([], new MemorySync());
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
it("creates a user", async () => {
|
|
22
|
-
let user = await
|
|
23
|
+
let user = await dao.createUser(data);
|
|
23
24
|
|
|
24
25
|
expect(user.username).toEqual(data.username);
|
|
25
26
|
// The value should be encrypted now
|
|
@@ -29,7 +30,7 @@ describe("simple-users-dao", () => {
|
|
|
29
30
|
expect(user.created_at).not.toBeUndefined();
|
|
30
31
|
expect(user.last_updated).not.toBeUndefined();
|
|
31
32
|
|
|
32
|
-
const refresh =
|
|
33
|
+
const refresh = dao.getUserByUsername(data.username);
|
|
33
34
|
|
|
34
35
|
// If we retrieve the user by username, it matches the object we got when we created it
|
|
35
36
|
expect(refresh).toMatchObject(user);
|
|
@@ -40,9 +41,9 @@ describe("simple-users-dao", () => {
|
|
|
40
41
|
|
|
41
42
|
try {
|
|
42
43
|
// Create the user
|
|
43
|
-
await
|
|
44
|
+
await dao.createUser(data);
|
|
44
45
|
// Attempt to create the user again, this will fail
|
|
45
|
-
await
|
|
46
|
+
await dao.createUser(data);
|
|
46
47
|
} catch (err) {
|
|
47
48
|
hasError = err;
|
|
48
49
|
}
|
|
@@ -52,9 +53,9 @@ describe("simple-users-dao", () => {
|
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
it("retrieve user by username and password", async () => {
|
|
55
|
-
const user1 = await
|
|
56
|
+
const user1 = await dao.createUser(data);
|
|
56
57
|
|
|
57
|
-
const user2 =
|
|
58
|
+
const user2 = dao.getUserByUsernameAndPassword(
|
|
58
59
|
data.username,
|
|
59
60
|
data.password
|
|
60
61
|
);
|
|
@@ -63,9 +64,9 @@ describe("simple-users-dao", () => {
|
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
it("retrieve user by username and wrong password fails", async () => {
|
|
66
|
-
await
|
|
67
|
+
await dao.createUser(data);
|
|
67
68
|
|
|
68
|
-
const user2 =
|
|
69
|
+
const user2 = dao.getUserByUsernameAndPassword(
|
|
69
70
|
data.username,
|
|
70
71
|
"wrong password"
|
|
71
72
|
);
|
|
@@ -74,22 +75,35 @@ describe("simple-users-dao", () => {
|
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
it("retrieve user by username and undefined password fails", async () => {
|
|
77
|
-
await
|
|
78
|
+
await dao.createUser(data);
|
|
78
79
|
|
|
79
|
-
const user2 =
|
|
80
|
+
const user2 = dao.getUserByUsernameAndPassword(data.username, undefined);
|
|
80
81
|
|
|
81
82
|
expect(user2).toBe(false);
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
it("retrieve user by username that does not exist fails", async () => {
|
|
85
|
-
const user =
|
|
86
|
+
const user = dao.getUserByUsernameAndPassword("notarealuser", "password");
|
|
86
87
|
|
|
87
88
|
expect(user).toBe(false);
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
it("retrieve user by username that is undefined fails", async () => {
|
|
91
|
-
const user =
|
|
92
|
+
const user = dao.getUserByUsernameAndPassword(undefined, "password");
|
|
92
93
|
|
|
93
94
|
expect(user).toBe(false);
|
|
94
95
|
});
|
|
96
|
+
|
|
97
|
+
it("deletes a user by id", async () => {
|
|
98
|
+
const user = await dao.createUser(data);
|
|
99
|
+
|
|
100
|
+
expect(dao.getDB().data.users).toHaveLength(1);
|
|
101
|
+
|
|
102
|
+
const deleted = dao.deleteUser(user.id);
|
|
103
|
+
|
|
104
|
+
expect(deleted).toBe(true);
|
|
105
|
+
expect(dao.getUserById(user.id)).toBeUndefined();
|
|
106
|
+
|
|
107
|
+
expect(dao.getDB().data.users).toHaveLength(0);
|
|
108
|
+
});
|
|
95
109
|
});
|
|
@@ -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,4 @@ 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 { default as LowDBUserDao } 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
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
import request from "supertest";
|
|
5
5
|
import { MemorySync } from "lowdb";
|
|
6
6
|
import createAppServer from "../createAppServer";
|
|
7
|
-
import
|
|
7
|
+
import LowDBUserDao from "../data/lowdb-user-dao.js";
|
|
8
8
|
|
|
9
9
|
// This suppresses a warning we don't need in tests
|
|
10
10
|
process.env.JWT_SECRET = "SECRET";
|
|
11
11
|
|
|
12
|
-
let
|
|
12
|
+
let dao = new LowDBUserDao([], new MemorySync());
|
|
13
13
|
|
|
14
14
|
// We want to explicitly test functionality we disable during testing
|
|
15
15
|
process.env.NODE_ENV = "override";
|
|
16
|
-
const app = createAppServer({
|
|
16
|
+
const app = createAppServer({ dao, start: false });
|
|
17
17
|
process.env.NODE_ENV = "test";
|
|
18
18
|
|
|
19
19
|
describe("/auth", () => {
|
|
@@ -21,7 +21,7 @@ describe("/auth", () => {
|
|
|
21
21
|
|
|
22
22
|
beforeAll(async () => {
|
|
23
23
|
// Reset our users database before each test
|
|
24
|
-
const user = await
|
|
24
|
+
const user = await dao.createUser({
|
|
25
25
|
username: "test",
|
|
26
26
|
password: "test",
|
|
27
27
|
});
|
|
@@ -116,7 +116,7 @@ describe("/auth", () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
it("GET /verify verifies user", async () => {
|
|
119
|
-
const user =
|
|
119
|
+
const user = dao.getUserById(userId);
|
|
120
120
|
|
|
121
121
|
expect(user.verification_token).not.toBe(null);
|
|
122
122
|
expect(user.verified_date).toBeUndefined();
|
|
@@ -131,7 +131,7 @@ describe("/auth", () => {
|
|
|
131
131
|
expect(res.body.success).toEqual(true);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
const refresh = await
|
|
134
|
+
const refresh = await dao.getUserById(userId);
|
|
135
135
|
|
|
136
136
|
expect(refresh.verified_date).not.toBe(null);
|
|
137
137
|
|