@vue-skuilder/express 0.1.24 → 0.1.26
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/dist/app-factory.d.ts.map +1 -1
- package/dist/app-factory.js +26 -11
- package/dist/app-factory.js.map +1 -1
- package/dist/attachment-preprocessing/normalize-local.js +31 -4
- package/dist/attachment-preprocessing/normalize-local.js.map +1 -1
- package/dist/client-requests/pack-requests.d.ts.map +1 -1
- package/dist/client-requests/pack-requests.js +14 -10
- package/dist/client-requests/pack-requests.js.map +1 -1
- package/dist/couchdb/authentication.d.ts +35 -1
- package/dist/couchdb/authentication.d.ts.map +1 -1
- package/dist/couchdb/authentication.js +87 -52
- package/dist/couchdb/authentication.js.map +1 -1
- package/dist/peruser.d.ts +14 -0
- package/dist/peruser.d.ts.map +1 -0
- package/dist/peruser.js +75 -0
- package/dist/peruser.js.map +1 -0
- package/package.json +4 -4
- package/src/app-factory.ts +61 -29
- package/src/attachment-preprocessing/normalize-local.ts +38 -4
- package/src/client-requests/pack-requests.ts +74 -42
- package/src/couchdb/authentication.ts +107 -54
- package/src/peruser.ts +80 -0
|
@@ -16,76 +16,129 @@ interface CouchSession {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} else {
|
|
28
|
-
return await Nano({
|
|
29
|
-
cookie: 'AuthSession=' + authCookie,
|
|
30
|
-
url: getCouchURLWithProtocol(),
|
|
31
|
-
})
|
|
32
|
-
.session()
|
|
33
|
-
.then((s: CouchSession) => {
|
|
34
|
-
logger.info(`AuthUser: ${JSON.stringify(s)}`);
|
|
35
|
-
const isAdmin = s.userCtx.roles.indexOf('_admin') !== -1;
|
|
36
|
-
const isLoggedInUser = s.userCtx.name === username;
|
|
37
|
-
return isAdmin && isLoggedInUser;
|
|
38
|
-
})
|
|
39
|
-
.catch((_err) => {
|
|
40
|
-
return false;
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function logRequest(req: VueClientRequest) {
|
|
46
|
-
logger.info(`${req.body.type} request from ${req.body.user}...`);
|
|
47
|
-
}
|
|
19
|
+
/**
|
|
20
|
+
* Result of authentication check.
|
|
21
|
+
* - If authenticated: { authenticated: true, username: string, isAdmin: boolean }
|
|
22
|
+
* - If not authenticated: { authenticated: false }
|
|
23
|
+
*/
|
|
24
|
+
export type AuthResult =
|
|
25
|
+
| { authenticated: true; username: string; isAdmin: boolean }
|
|
26
|
+
| { authenticated: false };
|
|
48
27
|
|
|
49
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Get the authenticated user from the request's session cookie or authorization header.
|
|
30
|
+
* Returns the username from the session - does NOT trust req.body.user.
|
|
31
|
+
*
|
|
32
|
+
* This is the secure way to get the authenticated username - callers should use
|
|
33
|
+
* the returned username rather than trusting user-supplied data.
|
|
34
|
+
*/
|
|
35
|
+
export async function getAuthenticatedUser(
|
|
36
|
+
req: VueClientRequest
|
|
37
|
+
): Promise<AuthResult> {
|
|
50
38
|
logRequest(req);
|
|
51
39
|
|
|
52
40
|
// Studio mode bypass: skip authentication for local development
|
|
53
41
|
if (process.env.NODE_ENV === 'studio') {
|
|
54
42
|
logger.info('Studio mode: bypassing authentication for local development');
|
|
55
|
-
|
|
43
|
+
// In studio mode, trust the body.user since this is local development only
|
|
44
|
+
const studioUser = req.body.user || 'studio-user';
|
|
45
|
+
return { authenticated: true, username: studioUser, isAdmin: true };
|
|
56
46
|
}
|
|
57
47
|
|
|
48
|
+
// Check for Basic auth header first
|
|
58
49
|
if (req.headers.authorization) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
.split('
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
const authHeader = req.headers.authorization;
|
|
51
|
+
if (authHeader.startsWith('Basic ')) {
|
|
52
|
+
const auth = Buffer.from(authHeader.split(' ')[1], 'base64')
|
|
53
|
+
.toString('ascii')
|
|
54
|
+
.split(':');
|
|
55
|
+
const username = auth[0];
|
|
56
|
+
const password = auth[1];
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
try {
|
|
59
|
+
const authResult = await Nano({
|
|
60
|
+
url: getCouchURLWithProtocol(),
|
|
61
|
+
}).auth(username, password);
|
|
68
62
|
|
|
69
|
-
|
|
63
|
+
if (authResult.ok) {
|
|
64
|
+
// Check if user has admin role by getting session
|
|
65
|
+
const nano = Nano({
|
|
66
|
+
url: getCouchURLWithProtocol(),
|
|
67
|
+
});
|
|
68
|
+
await nano.auth(username, password);
|
|
69
|
+
const session = (await nano.session()) as CouchSession;
|
|
70
|
+
const isAdmin = session.userCtx.roles.includes('_admin');
|
|
71
|
+
return { authenticated: true, username, isAdmin };
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger.warn('Basic auth failed:', err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { authenticated: false };
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
const authCookie: string = req.cookies.AuthSession
|
|
80
|
+
// Check for session cookie
|
|
81
|
+
const authCookie: string = req.cookies.AuthSession
|
|
82
|
+
? req.cookies.AuthSession
|
|
83
|
+
: 'null';
|
|
74
84
|
|
|
75
85
|
if (authCookie === 'null') {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
86
|
+
return { authenticated: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const session = (await Nano({
|
|
79
91
|
cookie: 'AuthSession=' + authCookie,
|
|
80
92
|
url: getCouchURLWithProtocol(),
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
}).session()) as CouchSession;
|
|
94
|
+
|
|
95
|
+
logger.info(`AuthUser: ${JSON.stringify(session)}`);
|
|
96
|
+
|
|
97
|
+
if (session.ok && session.userCtx.name) {
|
|
98
|
+
const isAdmin = session.userCtx.roles.includes('_admin');
|
|
99
|
+
return {
|
|
100
|
+
authenticated: true,
|
|
101
|
+
username: session.userCtx.name,
|
|
102
|
+
isAdmin,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { authenticated: false };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.warn('Session validation failed:', err);
|
|
109
|
+
return { authenticated: false };
|
|
90
110
|
}
|
|
91
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if the request is authenticated (any valid user).
|
|
115
|
+
* Returns true if authenticated, false otherwise.
|
|
116
|
+
*
|
|
117
|
+
* @deprecated Use getAuthenticatedUser() instead to get the actual username
|
|
118
|
+
*/
|
|
119
|
+
export async function requestIsAuthenticated(
|
|
120
|
+
req: VueClientRequest
|
|
121
|
+
): Promise<boolean> {
|
|
122
|
+
const result = await getAuthenticatedUser(req);
|
|
123
|
+
return result.authenticated;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if the request is authenticated as an admin user.
|
|
128
|
+
* Returns true only if the session has _admin role.
|
|
129
|
+
*
|
|
130
|
+
* SECURITY FIX: No longer trusts req.body.user - only checks session role.
|
|
131
|
+
*
|
|
132
|
+
* @deprecated Use getAuthenticatedUser() and check isAdmin instead
|
|
133
|
+
*/
|
|
134
|
+
export async function requestIsAdminAuthenticated(
|
|
135
|
+
req: VueClientRequest
|
|
136
|
+
): Promise<boolean> {
|
|
137
|
+
const result = await getAuthenticatedUser(req);
|
|
138
|
+
return result.authenticated && result.isAdmin;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function logRequest(req: VueClientRequest) {
|
|
142
|
+
// Log the claimed user for debugging, but note this is NOT trusted
|
|
143
|
+
logger.info(`${req.body.type} request (claimed user: ${req.body.user})...`);
|
|
144
|
+
}
|
package/src/peruser.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-user database provisioning daemon.
|
|
3
|
+
*
|
|
4
|
+
* Replaces CouchDB's built-in couch_peruser Erlang module, which has a
|
|
5
|
+
* process leak bug that causes unbounded memory growth:
|
|
6
|
+
* https://github.com/apache/couchdb/issues/5871
|
|
7
|
+
*
|
|
8
|
+
* Watches the _users database changes feed and ensures that every user
|
|
9
|
+
* has a corresponding userdb-{hex(username)} database with appropriate
|
|
10
|
+
* security settings. Replays from since=0 on every startup — this is
|
|
11
|
+
* cheap and removes the need to persist checkpoint state.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getCouchDB, type SecurityObject } from './couchdb/index.js';
|
|
15
|
+
import logger from './logger.js';
|
|
16
|
+
|
|
17
|
+
function hexEncode(str: string): string {
|
|
18
|
+
let returnStr = '';
|
|
19
|
+
for (let i = 0; i < str.length; i++) {
|
|
20
|
+
returnStr += ('000' + str.charCodeAt(i).toString(16)).slice(3);
|
|
21
|
+
}
|
|
22
|
+
return returnStr;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function ensureUserDB(username: string): Promise<void> {
|
|
26
|
+
const dbName = `userdb-${hexEncode(username)}`;
|
|
27
|
+
const server = getCouchDB();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await server.db.create(dbName);
|
|
31
|
+
logger.info(`peruser: created ${dbName}`);
|
|
32
|
+
} catch (e: unknown) {
|
|
33
|
+
if (
|
|
34
|
+
e &&
|
|
35
|
+
typeof e === 'object' &&
|
|
36
|
+
'statusCode' in e &&
|
|
37
|
+
(e as { statusCode: number }).statusCode === 412
|
|
38
|
+
) {
|
|
39
|
+
// already exists — expected on replay
|
|
40
|
+
} else {
|
|
41
|
+
logger.error(`peruser: error creating ${dbName}:`, e);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const db = server.use(dbName);
|
|
47
|
+
try {
|
|
48
|
+
const security: SecurityObject = {
|
|
49
|
+
admins: { names: [username], roles: [] },
|
|
50
|
+
members: { names: [username], roles: [] },
|
|
51
|
+
};
|
|
52
|
+
await db.insert(security, '_security');
|
|
53
|
+
} catch (e) {
|
|
54
|
+
logger.error(`peruser: error setting security on ${dbName}:`, e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function startPerUserProvisioning(): void {
|
|
59
|
+
const usersDB = getCouchDB().use('_users');
|
|
60
|
+
|
|
61
|
+
usersDB.changesReader
|
|
62
|
+
.start({
|
|
63
|
+
includeDocs: false,
|
|
64
|
+
since: '0',
|
|
65
|
+
})
|
|
66
|
+
.on('change', (change: { id: string; deleted?: boolean }) => {
|
|
67
|
+
if (!change.id.startsWith('org.couchdb.user:')) return;
|
|
68
|
+
if (change.deleted) return;
|
|
69
|
+
|
|
70
|
+
const username = change.id.replace('org.couchdb.user:', '');
|
|
71
|
+
ensureUserDB(username).catch((e) => {
|
|
72
|
+
logger.error(`peruser: unhandled error for ${username}:`, e);
|
|
73
|
+
});
|
|
74
|
+
})
|
|
75
|
+
.on('error', (err: Error) => {
|
|
76
|
+
logger.error('peruser: _users changes feed error:', err);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
logger.info('peruser: watching _users for new accounts');
|
|
80
|
+
}
|