feat(identity): redesign identity module and introduce RBAC foundation
- Redesign the Identity module with a richer domain model. - Extend the User entity to support username, Authentik integration, activity tracking, and storage information. - Add Role and Permission domain models with many-to-many relationships. - Implement RBAC foundation using UserRole, RolePermission, and UserPermission mappings. - Add user storage quota and usage fields with default values. - Introduce Authentik identifiers and synchronization metadata. - Refactor user domain logic for role and permission management. - Update Prisma schema to support the new identity architecture. - Improve JWT authentication and permission guard integration. - Update repositories, handlers, controllers, mappers, DTOs, and Swagger configuration. - Refresh environment configuration and project dependencies.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"c6e079ee66bcf8f961dc-c23b04c7b0e0ed54fd6c",
|
||||
"c6e079ee66bcf8f961dc-eb7b68d84e422e1710d8"
|
||||
]
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: user.integration.spec.ts >> Integration - User lifecycle >> login via Authentik creates local user and issues internal JWT
|
||||
- Location: tests\integration\user.integration.spec.ts:82:7
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: No jwt cookie set after login. Saved screenshot/cookies/page to tests/test-artifacts
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
19 | // increase default timeout for slow integration flows
|
||||
20 | test.setTimeout(60000);
|
||||
21 |
|
||||
22 | async function adminLogin() {
|
||||
23 | const adminUser = process.env.ADMIN_USERNAME;
|
||||
24 | const adminPass = process.env.ADMIN_PASSWORD;
|
||||
25 | if (!adminUser || !adminPass) throw new Error('ADMIN_USERNAME/ADMIN_PASSWORD must be set in env for integration tests');
|
||||
26 |
|
||||
27 | const browser = await chromium.launch({ headless: true });
|
||||
28 | const context = await browser.newContext();
|
||||
29 | const page = await context.newPage();
|
||||
30 | await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||
31 |
|
||||
32 | try {
|
||||
33 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 });
|
||||
34 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]');
|
||||
35 | if (usernameInput) await usernameInput.fill(adminUser);
|
||||
36 | const passwordInput = await page.$('input[type="password"], input[name="password"]');
|
||||
37 | if (passwordInput) await passwordInput.fill(adminPass);
|
||||
38 | const submitButton = await page.$('button[type="submit"], input[type="submit"]');
|
||||
39 | if (submitButton) {
|
||||
40 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]);
|
||||
41 | }
|
||||
42 | } catch (e) {
|
||||
43 | // ignore if login form not present
|
||||
44 | }
|
||||
45 |
|
||||
46 | await page.waitForTimeout(1000);
|
||||
47 | const cookies = await context.cookies();
|
||||
48 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt');
|
||||
49 | await context.close();
|
||||
50 | await browser.close();
|
||||
51 |
|
||||
52 | if (!jwtCookie) throw new Error('Admin login failed: no session cookie');
|
||||
53 | return jwtCookie.value;
|
||||
54 | }
|
||||
55 |
|
||||
56 | async function apiRequest(path, token, opts = {}) {
|
||||
57 | const headers = Object.assign({ 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, opts.headers || {});
|
||||
58 | const method = opts.method || 'GET';
|
||||
59 | const body = opts.body ? JSON.stringify(opts.body) : undefined;
|
||||
60 | let url;
|
||||
61 | try {
|
||||
62 | url = new URL(path, BASE_URL).toString();
|
||||
63 | } catch (e) {
|
||||
64 | throw new Error(`Invalid BASE_URL for integration tests: ${BASE_URL}`);
|
||||
65 | }
|
||||
66 | const res = await fetch(url, { method, headers, body });
|
||||
67 | let json = null;
|
||||
68 | try { json = await res.json(); } catch (e) { json = null; }
|
||||
69 | return { status: res.status, body: json };
|
||||
70 | }
|
||||
71 |
|
||||
72 |
|
||||
73 | test.describe('Integration - User lifecycle', () => {
|
||||
74 | let adminToken;
|
||||
75 | let createdUser = null;
|
||||
76 |
|
||||
77 | test.beforeAll(async () => {
|
||||
78 | // Integration tests assume test users are created in Authentik prior to running.
|
||||
79 | // RayLab will create local user record upon first successful login via Authentik.
|
||||
80 | });
|
||||
81 |
|
||||
82 | test('login via Authentik creates local user and issues internal JWT', async () => {
|
||||
83 | // use browser flow to login as the test user
|
||||
84 | const browser = await chromium.launch({ headless: true });
|
||||
85 | const context = await browser.newContext();
|
||||
86 | const page = await context.newPage();
|
||||
87 | await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||
88 |
|
||||
89 | try {
|
||||
90 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 });
|
||||
91 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]');
|
||||
92 | if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME);
|
||||
93 | const passwordInput = await page.$('input[type="password"], input[name="password"]');
|
||||
94 | if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD);
|
||||
95 | const submitButton = await page.$('button[type="submit"], input[type="submit"]');
|
||||
96 | if (submitButton) {
|
||||
97 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]);
|
||||
98 | }
|
||||
99 | } catch (e) {
|
||||
100 | // ignore if login form not present
|
||||
101 | }
|
||||
102 |
|
||||
103 | await page.waitForTimeout(1000);
|
||||
104 | const cookies = await context.cookies();
|
||||
105 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt');
|
||||
106 | const refreshCookie = cookies.find(c => c.name === 'raylab_refresh');
|
||||
107 |
|
||||
108 | if (!jwtCookie) {
|
||||
109 | const fs = require('fs');
|
||||
110 | const dir = 'tests/test-artifacts';
|
||||
111 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
112 | const ts = Date.now();
|
||||
113 | await page.screenshot({ path: `${dir}/failed-login-${ts}.png`, fullPage: true });
|
||||
114 | fs.writeFileSync(`${dir}/failed-login-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8');
|
||||
115 | const content = await page.content();
|
||||
116 | fs.writeFileSync(`${dir}/failed-login-${ts}-page.html`, content, 'utf-8');
|
||||
117 | await context.close();
|
||||
118 | await browser.close();
|
||||
> 119 | throw new Error(`No jwt cookie set after login. Saved screenshot/cookies/page to ${dir}`);
|
||||
| ^ Error: No jwt cookie set after login. Saved screenshot/cookies/page to tests/test-artifacts
|
||||
120 | }
|
||||
121 |
|
||||
122 | // ensure we have an internal jwt cookie
|
||||
123 | expect(jwtCookie).toBeDefined();
|
||||
124 | expect(jwtCookie.value).toBeTruthy();
|
||||
125 |
|
||||
126 | // validate /auth/me using the internal jwt
|
||||
127 | const token = jwtCookie.value;
|
||||
128 | const meUrl = new URL('/auth/me', BASE_URL).toString();
|
||||
129 | const meResp = await fetch(meUrl, { headers: { Authorization: `Bearer ${token}` } });
|
||||
130 | const meJson = await meResp.json();
|
||||
131 | expect(meResp.status).toBeLessThan(300);
|
||||
132 | expect(meJson.success).toBe(true);
|
||||
133 | expect(meJson.data).toBeDefined();
|
||||
134 | expect(meJson.data.email).toBe(TEST_USER_EMAIL);
|
||||
135 |
|
||||
136 | await context.close();
|
||||
137 | await browser.close();
|
||||
138 | });
|
||||
139 |
|
||||
140 | test('login via Authentik (created user) issues internal JWT', async () => {
|
||||
141 | // use browser flow to login as the created user
|
||||
142 | const browser = await chromium.launch({ headless: true });
|
||||
143 | const context = await browser.newContext();
|
||||
144 | const page = await context.newPage();
|
||||
145 | await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||
146 |
|
||||
147 | try {
|
||||
148 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 });
|
||||
149 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]');
|
||||
150 | if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME);
|
||||
151 | const passwordInput = await page.$('input[type="password"], input[name="password"]');
|
||||
152 | if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD);
|
||||
153 | const submitButton = await page.$('button[type="submit"], input[type="submit"]');
|
||||
154 | if (submitButton) {
|
||||
155 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]);
|
||||
156 | }
|
||||
157 | } catch (e) {
|
||||
158 | // ignore if login form not present
|
||||
159 | }
|
||||
160 |
|
||||
161 | await page.waitForTimeout(1000);
|
||||
162 | const cookies = await context.cookies();
|
||||
163 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt');
|
||||
164 |
|
||||
165 | if (!jwtCookie) {
|
||||
166 | const fs = require('fs');
|
||||
167 | const dir = 'tests/test-artifacts';
|
||||
168 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
169 | const ts = Date.now();
|
||||
170 | await page.screenshot({ path: `${dir}/failed-login-2-${ts}.png`, fullPage: true });
|
||||
171 | fs.writeFileSync(`${dir}/failed-login-2-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8');
|
||||
172 | const content = await page.content();
|
||||
173 | fs.writeFileSync(`${dir}/failed-login-2-${ts}-page.html`, content, 'utf-8');
|
||||
174 | await context.close();
|
||||
175 | await browser.close();
|
||||
176 | throw new Error(`No jwt cookie set after login (second test). Saved screenshot/cookies/page to ${dir}`);
|
||||
177 | }
|
||||
178 |
|
||||
179 | await context.close();
|
||||
180 | await browser.close();
|
||||
181 |
|
||||
182 | expect(jwtCookie).toBeDefined();
|
||||
183 | expect(jwtCookie.value).toBeTruthy();
|
||||
184 | });
|
||||
185 |
|
||||
186 | // Cleanup via API provisioning has been removed. If test environment requires cleanup, perform manually in Authentik.
|
||||
187 |
|
||||
188 | });
|
||||
189 |
|
||||
```
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: user.integration.spec.ts >> Integration - User lifecycle >> login via Authentik (created user) issues internal JWT
|
||||
- Location: tests\integration\user.integration.spec.ts:140:7
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: No jwt cookie set after login (second test). Saved screenshot/cookies/page to tests/test-artifacts
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
76 |
|
||||
77 | test.beforeAll(async () => {
|
||||
78 | // Integration tests assume test users are created in Authentik prior to running.
|
||||
79 | // RayLab will create local user record upon first successful login via Authentik.
|
||||
80 | });
|
||||
81 |
|
||||
82 | test('login via Authentik creates local user and issues internal JWT', async () => {
|
||||
83 | // use browser flow to login as the test user
|
||||
84 | const browser = await chromium.launch({ headless: true });
|
||||
85 | const context = await browser.newContext();
|
||||
86 | const page = await context.newPage();
|
||||
87 | await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||
88 |
|
||||
89 | try {
|
||||
90 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 });
|
||||
91 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]');
|
||||
92 | if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME);
|
||||
93 | const passwordInput = await page.$('input[type="password"], input[name="password"]');
|
||||
94 | if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD);
|
||||
95 | const submitButton = await page.$('button[type="submit"], input[type="submit"]');
|
||||
96 | if (submitButton) {
|
||||
97 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]);
|
||||
98 | }
|
||||
99 | } catch (e) {
|
||||
100 | // ignore if login form not present
|
||||
101 | }
|
||||
102 |
|
||||
103 | await page.waitForTimeout(1000);
|
||||
104 | const cookies = await context.cookies();
|
||||
105 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt');
|
||||
106 | const refreshCookie = cookies.find(c => c.name === 'raylab_refresh');
|
||||
107 |
|
||||
108 | if (!jwtCookie) {
|
||||
109 | const fs = require('fs');
|
||||
110 | const dir = 'tests/test-artifacts';
|
||||
111 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
112 | const ts = Date.now();
|
||||
113 | await page.screenshot({ path: `${dir}/failed-login-${ts}.png`, fullPage: true });
|
||||
114 | fs.writeFileSync(`${dir}/failed-login-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8');
|
||||
115 | const content = await page.content();
|
||||
116 | fs.writeFileSync(`${dir}/failed-login-${ts}-page.html`, content, 'utf-8');
|
||||
117 | await context.close();
|
||||
118 | await browser.close();
|
||||
119 | throw new Error(`No jwt cookie set after login. Saved screenshot/cookies/page to ${dir}`);
|
||||
120 | }
|
||||
121 |
|
||||
122 | // ensure we have an internal jwt cookie
|
||||
123 | expect(jwtCookie).toBeDefined();
|
||||
124 | expect(jwtCookie.value).toBeTruthy();
|
||||
125 |
|
||||
126 | // validate /auth/me using the internal jwt
|
||||
127 | const token = jwtCookie.value;
|
||||
128 | const meUrl = new URL('/auth/me', BASE_URL).toString();
|
||||
129 | const meResp = await fetch(meUrl, { headers: { Authorization: `Bearer ${token}` } });
|
||||
130 | const meJson = await meResp.json();
|
||||
131 | expect(meResp.status).toBeLessThan(300);
|
||||
132 | expect(meJson.success).toBe(true);
|
||||
133 | expect(meJson.data).toBeDefined();
|
||||
134 | expect(meJson.data.email).toBe(TEST_USER_EMAIL);
|
||||
135 |
|
||||
136 | await context.close();
|
||||
137 | await browser.close();
|
||||
138 | });
|
||||
139 |
|
||||
140 | test('login via Authentik (created user) issues internal JWT', async () => {
|
||||
141 | // use browser flow to login as the created user
|
||||
142 | const browser = await chromium.launch({ headless: true });
|
||||
143 | const context = await browser.newContext();
|
||||
144 | const page = await context.newPage();
|
||||
145 | await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||
146 |
|
||||
147 | try {
|
||||
148 | await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 });
|
||||
149 | const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]');
|
||||
150 | if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME);
|
||||
151 | const passwordInput = await page.$('input[type="password"], input[name="password"]');
|
||||
152 | if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD);
|
||||
153 | const submitButton = await page.$('button[type="submit"], input[type="submit"]');
|
||||
154 | if (submitButton) {
|
||||
155 | await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]);
|
||||
156 | }
|
||||
157 | } catch (e) {
|
||||
158 | // ignore if login form not present
|
||||
159 | }
|
||||
160 |
|
||||
161 | await page.waitForTimeout(1000);
|
||||
162 | const cookies = await context.cookies();
|
||||
163 | const jwtCookie = cookies.find(c => c.name === 'raylab_jwt');
|
||||
164 |
|
||||
165 | if (!jwtCookie) {
|
||||
166 | const fs = require('fs');
|
||||
167 | const dir = 'tests/test-artifacts';
|
||||
168 | if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
169 | const ts = Date.now();
|
||||
170 | await page.screenshot({ path: `${dir}/failed-login-2-${ts}.png`, fullPage: true });
|
||||
171 | fs.writeFileSync(`${dir}/failed-login-2-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8');
|
||||
172 | const content = await page.content();
|
||||
173 | fs.writeFileSync(`${dir}/failed-login-2-${ts}-page.html`, content, 'utf-8');
|
||||
174 | await context.close();
|
||||
175 | await browser.close();
|
||||
> 176 | throw new Error(`No jwt cookie set after login (second test). Saved screenshot/cookies/page to ${dir}`);
|
||||
| ^ Error: No jwt cookie set after login (second test). Saved screenshot/cookies/page to tests/test-artifacts
|
||||
177 | }
|
||||
178 |
|
||||
179 | await context.close();
|
||||
180 | await browser.close();
|
||||
181 |
|
||||
182 | expect(jwtCookie).toBeDefined();
|
||||
183 | expect(jwtCookie.value).toBeTruthy();
|
||||
184 | });
|
||||
185 |
|
||||
186 | // Cleanup via API provisioning has been removed. If test environment requires cleanup, perform manually in Authentik.
|
||||
187 |
|
||||
188 | });
|
||||
189 |
|
||||
```
|
||||
Reference in New Issue
Block a user