Skip to content

Commit 0b2fa82

Browse files
committed
Introducing the Setup Wizard for creating the first user
- no longer setup a default - still able to do that with env vars however
1 parent 6ab7198 commit 0b2fa82

File tree

29 files changed

+842
-635
lines changed

29 files changed

+842
-635
lines changed

backend/biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.2.3/schema.json",
33
"vcs": {
44
"enabled": true,
55
"clientKind": "git",

backend/internal/token.js

Lines changed: 59 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -18,67 +18,66 @@ export default {
1818
* @param {String} [issuer]
1919
* @returns {Promise}
2020
*/
21-
getTokenFromEmail: (data, issuer) => {
21+
getTokenFromEmail: async (data, issuer) => {
2222
const Token = TokenModel();
2323

2424
data.scope = data.scope || "user";
2525
data.expiry = data.expiry || "1d";
2626

27-
return userModel
27+
const user = await userModel
2828
.query()
2929
.where("email", data.identity.toLowerCase().trim())
3030
.andWhere("is_deleted", 0)
3131
.andWhere("is_disabled", 0)
32-
.first()
33-
.then((user) => {
34-
if (user) {
35-
// Get auth
36-
return authModel
37-
.query()
38-
.where("user_id", "=", user.id)
39-
.where("type", "=", "password")
40-
.first()
41-
.then((auth) => {
42-
if (auth) {
43-
return auth.verifyPassword(data.secret).then((valid) => {
44-
if (valid) {
45-
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
46-
// The scope requested doesn't exist as a role against the user,
47-
// you shall not pass.
48-
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
49-
}
50-
51-
// Create a moment of the expiry expression
52-
const expiry = parseDatePeriod(data.expiry);
53-
if (expiry === null) {
54-
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
55-
}
56-
57-
return Token.create({
58-
iss: issuer || "api",
59-
attrs: {
60-
id: user.id,
61-
},
62-
scope: [data.scope],
63-
expiresIn: data.expiry,
64-
}).then((signed) => {
65-
return {
66-
token: signed.token,
67-
expires: expiry.toISOString(),
68-
};
69-
});
70-
}
71-
throw new errs.AuthError(
72-
ERROR_MESSAGE_INVALID_AUTH,
73-
ERROR_MESSAGE_INVALID_AUTH_I18N,
74-
);
75-
});
76-
}
77-
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
78-
});
79-
}
80-
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
81-
});
32+
.first();
33+
34+
if (!user) {
35+
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
36+
}
37+
38+
const auth = await authModel
39+
.query()
40+
.where("user_id", "=", user.id)
41+
.where("type", "=", "password")
42+
.first();
43+
44+
if (!auth) {
45+
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
46+
}
47+
48+
const valid = await auth.verifyPassword(data.secret);
49+
if (!valid) {
50+
throw new errs.AuthError(
51+
ERROR_MESSAGE_INVALID_AUTH,
52+
ERROR_MESSAGE_INVALID_AUTH_I18N,
53+
);
54+
}
55+
56+
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
57+
// The scope requested doesn't exist as a role against the user,
58+
// you shall not pass.
59+
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
60+
}
61+
62+
// Create a moment of the expiry expression
63+
const expiry = parseDatePeriod(data.expiry);
64+
if (expiry === null) {
65+
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
66+
}
67+
68+
const signed = await Token.create({
69+
iss: issuer || "api",
70+
attrs: {
71+
id: user.id,
72+
},
73+
scope: [data.scope],
74+
expiresIn: data.expiry,
75+
});
76+
77+
return {
78+
token: signed.token,
79+
expires: expiry.toISOString(),
80+
};
8281
},
8382

8483
/**
@@ -88,7 +87,7 @@ export default {
8887
* @param {String} [data.scope] Only considered if existing token scope is admin
8988
* @returns {Promise}
9089
*/
91-
getFreshToken: (access, data) => {
90+
getFreshToken: async (access, data) => {
9291
const Token = TokenModel();
9392
const thisData = data || {};
9493

@@ -115,17 +114,17 @@ export default {
115114
}
116115
}
117116

118-
return Token.create({
117+
const signed = await Token.create({
119118
iss: "api",
120119
scope: scope,
121120
attrs: token_attrs,
122121
expiresIn: thisData.expiry,
123-
}).then((signed) => {
124-
return {
125-
token: signed.token,
126-
expires: expiry.toISOString(),
127-
};
128122
});
123+
124+
return {
125+
token: signed.token,
126+
expires: expiry.toISOString(),
127+
};
129128
}
130129
throw new error.AssertionFailedError("Existing token contained invalid user data");
131130
},
@@ -136,7 +135,7 @@ export default {
136135
*/
137136
getTokenFromUser: async (user) => {
138137
const expire = "1d";
139-
const Token = new TokenModel();
138+
const Token = TokenModel();
140139
const expiry = parseDatePeriod(expire);
141140

142141
const signed = await Token.create({

backend/internal/user.js

Lines changed: 42 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@ import internalToken from "./token.js";
1010

1111
const omissions = () => {
1212
return ["is_deleted"];
13-
}
13+
};
1414

15-
const DEFAULT_AVATAR = 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=200&d=mp&r=g';
15+
const DEFAULT_AVATAR = gravatar.url("admin@example.com", { default: "mm" });
1616

1717
const internalUser = {
1818
/**
19+
* Create a user can happen unauthenticated only once and only when no active users exist.
20+
* Otherwise, a valid auth method is required.
21+
*
1922
* @param {Access} access
2023
* @param {Object} data
2124
* @returns {Promise}
2225
*/
23-
create: (access, data) => {
26+
create: async (access, data) => {
2427
const auth = data.auth || null;
2528
delete data.auth;
2629

@@ -31,61 +34,43 @@ const internalUser = {
3134
data.is_disabled = data.is_disabled ? 1 : 0;
3235
}
3336

34-
return access
35-
.can("users:create", data)
36-
.then(() => {
37-
data.avatar = gravatar.url(data.email, { default: "mm" });
38-
return userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
39-
})
40-
.then((user) => {
41-
if (auth) {
42-
return authModel
43-
.query()
44-
.insert({
45-
user_id: user.id,
46-
type: auth.type,
47-
secret: auth.secret,
48-
meta: {},
49-
})
50-
.then(() => {
51-
return user;
52-
});
53-
}
54-
return user;
55-
})
56-
.then((user) => {
57-
// Create permissions row as well
58-
const is_admin = data.roles.indexOf("admin") !== -1;
37+
await access.can("users:create", data);
38+
data.avatar = gravatar.url(data.email, { default: "mm" });
5939

60-
return userPermissionModel
61-
.query()
62-
.insert({
63-
user_id: user.id,
64-
visibility: is_admin ? "all" : "user",
65-
proxy_hosts: "manage",
66-
redirection_hosts: "manage",
67-
dead_hosts: "manage",
68-
streams: "manage",
69-
access_lists: "manage",
70-
certificates: "manage",
71-
})
72-
.then(() => {
73-
return internalUser.get(access, { id: user.id, expand: ["permissions"] });
74-
});
75-
})
76-
.then((user) => {
77-
// Add to audit log
78-
return internalAuditLog
79-
.add(access, {
80-
action: "created",
81-
object_type: "user",
82-
object_id: user.id,
83-
meta: user,
84-
})
85-
.then(() => {
86-
return user;
87-
});
40+
let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
41+
if (auth) {
42+
user = await authModel.query().insert({
43+
user_id: user.id,
44+
type: auth.type,
45+
secret: auth.secret,
46+
meta: {},
8847
});
48+
}
49+
50+
// Create permissions row as well
51+
const isAdmin = data.roles.indexOf("admin") !== -1;
52+
53+
await userPermissionModel.query().insert({
54+
user_id: user.id,
55+
visibility: isAdmin ? "all" : "user",
56+
proxy_hosts: "manage",
57+
redirection_hosts: "manage",
58+
dead_hosts: "manage",
59+
streams: "manage",
60+
access_lists: "manage",
61+
certificates: "manage",
62+
});
63+
64+
user = await internalUser.get(access, { id: user.id, expand: ["permissions"] });
65+
66+
await internalAuditLog.add(access, {
67+
action: "created",
68+
object_type: "user",
69+
object_id: user.id,
70+
meta: user,
71+
});
72+
73+
return user;
8974
},
9075

9176
/**
@@ -316,11 +301,7 @@ const internalUser = {
316301
// Query is used for searching
317302
if (typeof search_query === "string") {
318303
query.where(function () {
319-
this.where("name", "like", `%${search_query}%`).orWhere(
320-
"email",
321-
"like",
322-
`%${search_query}%`,
323-
);
304+
this.where("name", "like", `%${search_query}%`).orWhere("email", "like", `%${search_query}%`);
324305
});
325306
}
326307

0 commit comments

Comments
 (0)