@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.
@@ -16,76 +16,129 @@ interface CouchSession {
16
16
  };
17
17
  }
18
18
 
19
- export async function requestIsAdminAuthenticated(req: VueClientRequest) {
20
- logRequest(req);
21
-
22
- const username = req.body.user;
23
- const authCookie: string = req.cookies.AuthSession ? req.cookies.AuthSession : 'null';
24
-
25
- if (authCookie === 'null') {
26
- return false;
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
- export async function requestIsAuthenticated(req: VueClientRequest) {
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
- return true;
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 auth = Buffer.from(req.headers.authorization.split(' ')[1], 'base64')
60
- .toString('ascii')
61
- .split(':');
62
- const username = auth[0];
63
- const password = auth[1];
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
- const authResult = await Nano({
66
- url: getCouchURLWithProtocol(),
67
- }).auth(username, password);
58
+ try {
59
+ const authResult = await Nano({
60
+ url: getCouchURLWithProtocol(),
61
+ }).auth(username, password);
68
62
 
69
- return authResult.ok;
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
- const username = req.body.user;
73
- const authCookie: string = req.cookies.AuthSession ? req.cookies.AuthSession : 'null';
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
- } else {
78
- return await Nano({
86
+ return { authenticated: false };
87
+ }
88
+
89
+ try {
90
+ const session = (await Nano({
79
91
  cookie: 'AuthSession=' + authCookie,
80
92
  url: getCouchURLWithProtocol(),
81
- })
82
- .session()
83
- .then((s: CouchSession) => {
84
- logger.info(`AuthUser: ${JSON.stringify(s)}`);
85
- return s.userCtx.name === username;
86
- })
87
- .catch((_err) => {
88
- return false;
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
+ }