diff --git a/.gitignore b/.gitignore index 9dd5fe0..a14e2ea 100755 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ backend/mappings.db backend/tasks.db backend/logs +backend/auth.db diff --git a/.kilocode/workflows/read_semantic.md b/.kilocode/workflows/read_semantic.md new file mode 100644 index 0000000..bde31a1 --- /dev/null +++ b/.kilocode/workflows/read_semantic.md @@ -0,0 +1,4 @@ +--- +description: USE SEMANTIC +--- +Прочитай semantic_protocol.md. ОБЯЗАТЕЛЬНО используй его при разработке \ No newline at end of file diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 069ab61..63156cb 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,8 +1,8 @@ diff --git a/frontend/src/components/auth/ProtectedRoute.svelte b/frontend/src/components/auth/ProtectedRoute.svelte new file mode 100644 index 0000000..a195689 --- /dev/null +++ b/frontend/src/components/auth/ProtectedRoute.svelte @@ -0,0 +1,61 @@ + + + + + +{#if $auth.loading} +
+
+
+{:else if $auth.isAuthenticated} + +{/if} + + \ No newline at end of file diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index c950422..2b960fe 100755 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -25,6 +25,22 @@ export const getWsUrl = (taskId) => { }; // [/DEF:getWsUrl:Function] +// [DEF:getAuthHeaders:Function] +// @PURPOSE: Returns headers with Authorization if token exists. +function getAuthHeaders() { + const headers = { + 'Content-Type': 'application/json', + }; + if (typeof window !== 'undefined') { + const token = localStorage.getItem('auth_token'); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + } + return headers; +} +// [/DEF:getAuthHeaders:Function] + // [DEF:fetchApi:Function] // @PURPOSE: Generic GET request wrapper. // @PRE: endpoint string is provided. @@ -34,7 +50,9 @@ export const getWsUrl = (taskId) => { async function fetchApi(endpoint) { try { console.log(`[api.fetchApi][Action] Fetching from context={{'endpoint': '${endpoint}'}}`); - const response = await fetch(`${API_BASE_URL}${endpoint}`); + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + headers: getAuthHeaders() + }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); } @@ -59,9 +77,7 @@ async function postApi(endpoint, body) { console.log(`[api.postApi][Action] Posting to context={{'endpoint': '${endpoint}'}}`); const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: getAuthHeaders(), body: JSON.stringify(body), }); if (!response.ok) { @@ -85,9 +101,7 @@ async function requestApi(endpoint, method = 'GET', body = null) { console.log(`[api.requestApi][Action] ${method} to context={{'endpoint': '${endpoint}'}}`); const options = { method, - headers: { - 'Content-Type': 'application/json', - }, + headers: getAuthHeaders(), }; if (body) { options.body = JSON.stringify(body); @@ -112,6 +126,9 @@ async function requestApi(endpoint, method = 'GET', body = null) { // [DEF:api:Data] // @PURPOSE: API client object with specific methods. export const api = { + fetchApi, + postApi, + requestApi, getPlugins: () => fetchApi('/plugins'), getTasks: () => fetchApi('/tasks'), getTask: (taskId) => fetchApi(`/tasks/${taskId}`), diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 4fd5373..e41db53 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -3,16 +3,28 @@ import Navbar from '../components/Navbar.svelte'; import Footer from '../components/Footer.svelte'; import Toast from '../components/Toast.svelte'; + import ProtectedRoute from '../components/auth/ProtectedRoute.svelte'; + import { page } from '$app/stores'; + + $: isLoginPage = $page.url.pathname === '/login';
- + {#if isLoginPage} +
+ +
+ {:else} + + -
- -
+
+ +
-
+
+ + {/if}
diff --git a/frontend/src/routes/admin/roles/+page.svelte b/frontend/src/routes/admin/roles/+page.svelte new file mode 100644 index 0000000..1924e66 --- /dev/null +++ b/frontend/src/routes/admin/roles/+page.svelte @@ -0,0 +1,216 @@ + + + + + + + +
+
+

{$t.admin.roles.title}

+ +
+ + {#if loading} +

{$t.admin.roles.loading}

+ {:else if error} +
{error}
+ {:else} +
+ + + + + + + + + + + {#each roles as role} + + + + + + + {/each} + +
{$t.admin.roles.name}{$t.admin.roles.description}{$t.admin.roles.permissions}{$t.common.actions}
{role.name}{role.description || '-'} +
+ {#each role.permissions as perm} + + {perm.resource}:{perm.action} + + {/each} +
+
+ + +
+
+ {/if} + + {#if showModal} +
+
+

+ {isEditing ? $t.admin.roles.modal_edit_title : $t.admin.roles.modal_create_title} +

+
+
+ + +
+
+ + +
+
+ +
+ {#each permissions as perm} + + {/each} +
+

{$t.admin.roles.permissions_hint}

+
+
+ + +
+
+
+
+ {/if} +
+ +
+ + + + \ No newline at end of file diff --git a/frontend/src/routes/admin/settings/+page.svelte b/frontend/src/routes/admin/settings/+page.svelte new file mode 100644 index 0000000..82f2c55 --- /dev/null +++ b/frontend/src/routes/admin/settings/+page.svelte @@ -0,0 +1,213 @@ + + + + + + + +
+
+

{$t.admin.settings.title}

+ +
+ + {#if loading} +
+

{$t.common.loading}

+
+ {:else if error} + + {:else} +
+ + + + + + + + + {#each mappings as mapping} + + + + + {/each} + {#if mappings.length === 0} + + + + {/if} + +
{$t.admin.settings.ad_group}{$t.admin.settings.local_role}
{mapping.ad_group} + + {roles.find(r => r.id === mapping.role_id)?.name || mapping.role_id} + +
+
+ + + +

{$t.admin.settings.no_mappings}

+
+
+
+ {/if} + + {#if showCreateModal} +
+
+

{$t.admin.settings.modal_title}

+
+
+ + +

{$t.admin.settings.ad_group_hint}

+
+
+ + +
+
+ + +
+
+
+
+ {/if} +
+ +
+ + + + \ No newline at end of file diff --git a/frontend/src/routes/admin/users/+page.svelte b/frontend/src/routes/admin/users/+page.svelte new file mode 100644 index 0000000..878cf33 --- /dev/null +++ b/frontend/src/routes/admin/users/+page.svelte @@ -0,0 +1,273 @@ + + + + + + + +
+
+

{$t.admin.users.title}

+ +
+ + {#if loading} +
+

{$t.common.loading}

+
+ {:else if error} +
+

{$t.common.error}

+

{error}

+
+ {:else} +
+ + + + + + + + + + + + + {#each users as user} + + + + + + + + + {/each} + +
{$t.admin.users.username}{$t.admin.users.email}{$t.admin.users.source}{$t.admin.users.roles}{$t.admin.users.status}{$t.common.actions}
{user.username}{user.email || '-'} + + {user.auth_source} + + +
+ {#each user.roles as role} + {role.name} + {/each} +
+
+ + + + {user.is_active ? $t.admin.users.active : $t.admin.users.inactive} + + + + + +
+
+ {/if} + + {#if showModal} +
+
+

+ {isEditing ? $t.admin.users.modal_edit_title : $t.admin.users.modal_title} +

+
+
+ + +
+
+ + +
+
+ + + {#if isEditing} +

{$t.admin.users.password_hint}

+ {/if} +
+
+ +
+
+ + +

{$t.admin.users.roles_hint}

+
+
+ + +
+
+
+
+ {/if} +
+ +
+ + + + diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte new file mode 100644 index 0000000..7c7dd97 --- /dev/null +++ b/frontend/src/routes/login/+page.svelte @@ -0,0 +1,165 @@ + + + + + + +
+

Login

+ + {#if error} +
+ {error} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ + +
+ +
+
+
+
+
+
+ Or continue with +
+
+ +
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index bbc7031..6062cb1 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -8,9 +8,29 @@ /** @type {import('./$types').PageData} */ export let data; - let settings = data.settings; + let settings = data.settings || { + environments: [], + settings: { + storage: { + root_path: '', + backup_structure_pattern: '', + repo_structure_pattern: '', + filename_pattern: '' + } + } + }; - $: settings = data.settings; + $: if (data.settings) { + settings = { ...data.settings }; + if (settings.settings && !settings.settings.storage) { + settings.settings.storage = { + root_path: '', + backup_structure_pattern: '', + repo_structure_pattern: '', + filename_pattern: '' + }; + } + } let newEnv = { id: '', diff --git a/frontend/src/routes/settings/+page.ts b/frontend/src/routes/settings/+page.ts index a4f4780..933f233 100644 --- a/frontend/src/routes/settings/+page.ts +++ b/frontend/src/routes/settings/+page.ts @@ -18,7 +18,13 @@ export async function load() { settings: { environments: [], settings: { - default_environment_id: null + default_environment_id: null, + storage: { + root_path: '', + backup_structure_pattern: '', + repo_structure_pattern: '', + filename_pattern: '' + } } }, error: 'Failed to load settings' diff --git a/frontend/src/services/adminService.js b/frontend/src/services/adminService.js new file mode 100644 index 0000000..cdcee00 --- /dev/null +++ b/frontend/src/services/adminService.js @@ -0,0 +1,240 @@ +// [DEF:adminService:Module] +// +// @SEMANTICS: admin, users, roles, ad-mappings, api +// @PURPOSE: Service for Admin-related API calls (User and Role management). +// @LAYER: Service +// @RELATION: DEPENDS_ON -> frontend.src.lib.api +// +// @INVARIANT: All requests must include valid Admin JWT token (handled by api client). + +// [SECTION: IMPORTS] +import { api } from '../lib/api'; +// [/SECTION] + +// [DEF:getUsers:Function] +/** + * @purpose Fetches all registered users from the backend. + * @pre User must be authenticated with Admin privileges. + * @post Returns an array of user objects. + * @returns {Promise} + * @relation CALLS -> backend.src.api.routes.admin.list_users + */ +async function getUsers() { + console.log('[getUsers][Entry]'); + try { + const users = await api.requestApi('/admin/users', 'GET'); + console.log('[getUsers][Coherence:OK]'); + return users; + } catch (e) { + console.error('[getUsers][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:getUsers:Function] + +// [DEF:createUser:Function] +/** + * @purpose Creates a new local user. + * @pre User must be authenticated with Admin privileges. + * @param {Object} userData - User details (username, email, password, roles, is_active). + * @post New user record created in auth.db. + * @returns {Promise} + * @relation CALLS -> backend.src.api.routes.admin.create_user + */ +async function createUser(userData) { + console.log('[createUser][Entry]'); + try { + const user = await api.postApi('/admin/users', userData); + console.log('[createUser][Coherence:OK]'); + return user; + } catch (e) { + console.error('[createUser][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:createUser:Function] + +// [DEF:getRoles:Function] +/** + * @purpose Fetches all available system roles. + * @returns {Promise} + * @relation CALLS -> backend.src.api.routes.admin.list_roles + */ +async function getRoles() { + console.log('[getRoles][Entry]'); + try { + const roles = await api.requestApi('/admin/roles', 'GET'); + console.log('[getRoles][Coherence:OK]'); + return roles; + } catch (e) { + console.error('[getRoles][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:getRoles:Function] + +// [DEF:getADGroupMappings:Function] +/** + * @purpose Fetches mappings between AD groups and local roles. + * @returns {Promise} + */ +async function getADGroupMappings() { + console.log('[getADGroupMappings][Entry]'); + try { + const mappings = await api.requestApi('/admin/ad-mappings', 'GET'); + console.log('[getADGroupMappings][Coherence:OK]'); + return mappings; + } catch (e) { + console.error('[getADGroupMappings][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:getADGroupMappings:Function] + +// [DEF:createADGroupMapping:Function] +/** + * @purpose Creates or updates an AD group to Role mapping. + * @param {Object} mappingData - Mapping details (ad_group, role_id). + * @returns {Promise} + */ +async function createADGroupMapping(mappingData) { + console.log('[createADGroupMapping][Entry]'); + try { + const mapping = await api.postApi('/admin/ad-mappings', mappingData); + console.log('[createADGroupMapping][Coherence:OK]'); + return mapping; + } catch (e) { + console.error('[createADGroupMapping][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:createADGroupMapping:Function] + +// [DEF:updateUser:Function] +/** + * @purpose Updates an existing user. + * @param {string} userId - Target user ID. + * @param {Object} userData - Updated user data. + * @returns {Promise} + */ +async function updateUser(userId, userData) { + console.log('[updateUser][Entry]'); + try { + const user = await api.requestApi(`/admin/users/${userId}`, 'PUT', userData); + console.log('[updateUser][Coherence:OK]'); + return user; + } catch (e) { + console.error('[updateUser][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:updateUser:Function] + +// [DEF:deleteUser:Function] +/** + * @purpose Deletes a user. + * @param {string} userId - Target user ID. + * @returns {Promise} + */ +async function deleteUser(userId) { + console.log('[deleteUser][Entry]'); + try { + await api.requestApi(`/admin/users/${userId}`, 'DELETE'); + console.log('[deleteUser][Coherence:OK]'); + } catch (e) { + console.error('[deleteUser][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:deleteUser:Function] + +// [DEF:createRole:Function] +/** + * @purpose Creates a new role. + * @param {Object} roleData - Role details (name, description, permissions). + * @returns {Promise} + */ +async function createRole(roleData) { + console.log('[createRole][Entry]'); + try { + const role = await api.postApi('/admin/roles', roleData); + console.log('[createRole][Coherence:OK]'); + return role; + } catch (e) { + console.error('[createRole][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:createRole:Function] + +// [DEF:updateRole:Function] +/** + * @purpose Updates an existing role. + * @param {string} roleId - Target role ID. + * @param {Object} roleData - Updated role data. + * @returns {Promise} + */ +async function updateRole(roleId, roleData) { + console.log('[updateRole][Entry]'); + try { + const role = await api.requestApi(`/admin/roles/${roleId}`, 'PUT', roleData); + console.log('[updateRole][Coherence:OK]'); + return role; + } catch (e) { + console.error('[updateRole][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:updateRole:Function] + +// [DEF:deleteRole:Function] +/** + * @purpose Deletes a role. + * @param {string} roleId - Target role ID. + * @returns {Promise} + */ +async function deleteRole(roleId) { + console.log('[deleteRole][Entry]'); + try { + await api.requestApi(`/admin/roles/${roleId}`, 'DELETE'); + console.log('[deleteRole][Coherence:OK]'); + } catch (e) { + console.error('[deleteRole][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:deleteRole:Function] + +// [DEF:getPermissions:Function] +/** + * @purpose Fetches all available permissions. + * @returns {Promise} + */ +async function getPermissions() { + console.log('[getPermissions][Entry]'); + try { + const permissions = await api.requestApi('/admin/permissions', 'GET'); + console.log('[getPermissions][Coherence:OK]'); + return permissions; + } catch (e) { + console.error('[getPermissions][Coherence:Failed]', e); + throw e; + } +} +// [/DEF:getPermissions:Function] + +export const adminService = { + getUsers, + createUser, + updateUser, + deleteUser, + getRoles, + createRole, + updateRole, + deleteRole, + getPermissions, + getADGroupMappings, + createADGroupMapping +}; + +// [/DEF:adminService:Module] \ No newline at end of file diff --git a/specs/016-multi-user-auth/plan.md b/specs/016-multi-user-auth/plan.md index e7f18d9..b8be2dd 100644 --- a/specs/016-multi-user-auth/plan.md +++ b/specs/016-multi-user-auth/plan.md @@ -78,10 +78,15 @@ frontend/ ├── src/ │ ├── lib/ │ │ ├── auth/ # New: Frontend auth stores/logic -│ │ └── api/ # Update: Add auth headers to requests +│ │ └── api.js # Update: Add auth headers and export core methods +│ ├── services/ +│ │ └── adminService.js # New: Service for admin API operations │ ├── routes/ │ │ ├── login/ # New: Login page -│ │ └── admin/ # New: Admin dashboard (Users/Roles) +│ │ └── admin/ +│ │ ├── users/ # New: User Management UI +│ │ ├── roles/ # New: Role Management UI +│ │ └── settings/ # New: ADFS Configuration UI │ └── components/ │ └── auth/ # New: Auth components (ProtectedRoute, Login form) ``` diff --git a/specs/016-multi-user-auth/spec.md b/specs/016-multi-user-auth/spec.md index fb0681a..1f0676a 100644 --- a/specs/016-multi-user-auth/spec.md +++ b/specs/016-multi-user-auth/spec.md @@ -49,6 +49,30 @@ As an administrator, I want to assign specific plugin access rights to users so --- +### User Story 4 - Role Management (Priority: P1) + +As an administrator, I want to create and manage roles with specific permissions so that I can easily assign standard access sets to users. + +**Why this priority**: Essential for scalable user management. Assigning individual permissions to every user is tedious and error-prone. + +**Acceptance Scenarios**: + +1. **Given** an administrator, **When** they navigate to the Role Management page, **Then** they see a list of all system roles. +2. **Given** an administrator, **When** they create a new role "Auditor" with "READ" permission on "Logs", **Then** the role is saved and available for assignment. +3. **Given** an administrator, **When** they update a role's permissions, **Then** all users with that role effectively gain/lose those permissions. + +**Why this priority**: Security is a core requirement. Without granular permissions, all authenticated users would have full administrative access, which defeats the purpose of multi-user support. + +**Independent Test**: Create two users with different permissions (e.g., User A has access to "Backup", User B does not). Verify User A can access the Backup tool while User B receives a 403 Forbidden error. + +**Acceptance Scenarios**: + +1. **Given** a user with "Backup" plugin permission, **When** they navigate to the Backup tool, **Then** the page loads successfully. +2. **Given** a user WITHOUT "Backup" plugin permission, **When** they navigate to the Backup tool, **Then** they are denied access (UI hides the link, API returns 403). +3. **Given** an administrator, **When** they edit a user's permissions, **Then** the changes take effect immediately or upon next login. + +--- + ### User Story 3 - ADFS Integration (Priority: P2) As a corporate user, I want to log in using my organization's ADFS credentials so that I don't have to manage a separate password. @@ -78,17 +102,18 @@ As a corporate user, I want to log in using my organization's ADFS credentials s - **FR-001**: System MUST support local user authentication via username and password. - **FR-002**: System MUST support authentication via ADFS (Active Directory Federation Services) using standard federation protocols. -- **FR-003**: System MUST provide a mechanism to manage users (Create, Read, Update, Delete) - restricted to administrators. +- **FR-003**: System MUST provide a web-based interface to manage users (Create, Read, Update, Delete) - restricted to administrators. - **FR-004**: System MUST implement Role-Based Access Control (RBAC) where permissions are assigned to Roles, and Roles are assigned to Users. - **FR-005**: System MUST enforce permissions at the server level for all plugin execution requests. - **FR-006**: System MUST enforce permissions at the user interface level (hide navigation items/buttons for unauthorized plugins). - **FR-007**: System MUST securely store local user credentials. - **FR-008**: System MUST support Just-In-Time (JIT) provisioning for ADFS users ONLY if they belong to a mapped AD group. - **FR-009**: System MUST provide a CLI utility to create an initial administrator account to prevent lockout during first deployment. -- **FR-010**: System MUST allow configuring mappings between Active Directory Groups and local System Roles. +- **FR-010**: System MUST provide a web-based interface for configuring mappings between Active Directory Groups and local System Roles. - **FR-011**: System MUST use JWT (JSON Web Tokens) for API session management. - **FR-012**: System MUST persist authentication and authorization data in a dedicated SQLite database (`auth.db`). - **FR-013**: System MUST provide a unified login interface supporting both Local (Username/Password) and ADFS (SSO Button) authentication methods simultaneously. +- **FR-014**: System MUST provide a web-based interface to manage Roles (Create, Update, Delete) and assign permissions to them. ### Key Entities diff --git a/specs/016-multi-user-auth/tasks.md b/specs/016-multi-user-auth/tasks.md index 5d7c91e..9b0ce59 100644 --- a/specs/016-multi-user-auth/tasks.md +++ b/specs/016-multi-user-auth/tasks.md @@ -8,74 +8,81 @@ *Goal: Initialize the auth database, core dependencies, and backend infrastructure.* -- [ ] T001 Install backend dependencies (Authlib, Passlib, PyJWT, SQLAlchemy) in `backend/requirements.txt` -- [ ] T002 Implement core configuration for Auth and Database in `backend/src/core/auth/config.py` -- [ ] T003 Implement database connection logic for `auth.db` in `backend/src/core/database.py` -- [ ] T004 Create SQLAlchemy models for User, Role, Permission in `backend/src/models/auth.py` -- [ ] T005 Create migration/init script to generate `auth.db` schema in `backend/src/scripts/init_auth_db.py` -- [ ] T006 Implement password hashing utility using Passlib in `backend/src/core/auth/security.py` -- [ ] T007 Implement JWT token generation and validation logic in `backend/src/core/auth/jwt.py` -- [ ] T008 [P] Implement CLI tool for creating the initial admin user in `backend/src/scripts/create_admin.py` +- [x] T001 Install backend dependencies (Authlib, Passlib, PyJWT, SQLAlchemy) in `backend/requirements.txt` +- [x] T002 Implement core configuration for Auth and Database in `backend/src/core/auth/config.py` +- [x] T003 Implement database connection logic for `auth.db` in `backend/src/core/database.py` +- [x] T004 Create SQLAlchemy models for User, Role, Permission in `backend/src/models/auth.py` +- [x] T005 Create migration/init script to generate `auth.db` schema in `backend/src/scripts/init_auth_db.py` +- [x] T006 Implement password hashing utility using Passlib in `backend/src/core/auth/security.py` +- [x] T007 Implement JWT token generation and validation logic in `backend/src/core/auth/jwt.py` +- [x] T008 [P] Implement CLI tool for creating the initial admin user in `backend/src/scripts/create_admin.py` ## Phase 2: User Story 1 - Local User Authentication (Priority: P1) *Goal: Enable users to log in with username/password and receive a JWT session.* -- [ ] T009 [US1] Create Pydantic schemas for User, UserCreate, Token in `backend/src/schemas/auth.py` -- [ ] T010 [US1] Implement `AuthRepository` for DB operations in `backend/src/core/auth/repository.py` -- [ ] T011 [US1] Implement `AuthService` for login logic (verify password, create token) in `backend/src/services/auth_service.py` -- [ ] T012 [US1] Create API endpoint `POST /api/auth/login` in `backend/src/api/auth.py` -- [ ] T013 [US1] Implement `get_current_user` dependency for JWT verification in `backend/src/dependencies.py` -- [ ] T014 [US1] Create API endpoint `GET /api/auth/me` to retrieve current user profile in `backend/src/api/auth.py` -- [ ] T043 [US1] Implement session revocation (Logout) endpoint in `backend/src/api/auth.py` -- [ ] T044 [US1] Implement account status check (`is_active`) in authentication flow in `backend/src/services/auth_service.py` -- [ ] T015 [US1] Implement frontend auth store (Svelte store) in `frontend/src/lib/auth/store.ts` -- [ ] T016 [US1] Implement Login Page UI using `src/lib/ui` and `src/lib/i18n` in `frontend/src/routes/login/+page.svelte` -- [ ] T017 [US1] Integrate Login Page with Backend API in `frontend/src/routes/login/+page.svelte` -- [ ] T018 [US1] Implement `ProtectedRoute` component to redirect unauthenticated users in `frontend/src/components/auth/ProtectedRoute.svelte` -- [ ] T037 [US1] Implement password complexity validation logic in `backend/src/core/auth/security.py` +- [x] T009 [US1] Create Pydantic schemas for User, UserCreate, Token in `backend/src/schemas/auth.py` +- [x] T010 [US1] Implement `AuthRepository` for DB operations in `backend/src/core/auth/repository.py` +- [x] T011 [US1] Implement `AuthService` for login logic (verify password, create token) in `backend/src/services/auth_service.py` +- [x] T012 [US1] Create API endpoint `POST /api/auth/login` in `backend/src/api/auth.py` +- [x] T013 [US1] Implement `get_current_user` dependency for JWT verification in `backend/src/dependencies.py` +- [x] T014 [US1] Create API endpoint `GET /api/auth/me` to retrieve current user profile in `backend/src/api/auth.py` +- [x] T043 [US1] Implement session revocation (Logout) endpoint in `backend/src/api/auth.py` +- [x] T044 [US1] Implement account status check (`is_active`) in authentication flow in `backend/src/services/auth_service.py` +- [x] T015 [US1] Implement frontend auth store (Svelte store) in `frontend/src/lib/auth/store.ts` +- [x] T016 [US1] Implement Login Page UI using `src/lib/ui` and `src/lib/i18n` in `frontend/src/routes/login/+page.svelte` +- [x] T017 [US1] Integrate Login Page with Backend API in `frontend/src/routes/login/+page.svelte` +- [x] T018 [US1] Implement `ProtectedRoute` component to redirect unauthenticated users in `frontend/src/components/auth/ProtectedRoute.svelte` +- [x] T037 [US1] Implement password complexity validation logic in `backend/src/core/auth/security.py` ## Phase 3: User Story 2 - Plugin-Based Access Control (Priority: P1) *Goal: Restrict access to plugins based on user roles and permissions.* -- [ ] T019 [US2] Update `PluginBase` to include required permission strings in `backend/src/core/plugin_base.py` -- [ ] T020 [US2] Implement `has_permission` dependency for route protection in `backend/src/dependencies.py` -- [ ] T021 [US2] Protect existing plugin API routes using `has_permission` in `backend/src/api/routes/*.py` -- [ ] T022 [US2] Implement `SystemAdminPlugin` inheriting from `PluginBase` for User/Role management in `backend/src/plugins/system_admin.py` -- [ ] T023 [US2] Implement Admin API endpoints within `SystemAdminPlugin` (with pagination) in `backend/src/api/routes/admin.py` -- [ ] T024 [US2] Create Admin Dashboard UI using `src/lib/ui` and `src/lib/i18n` in `frontend/src/routes/admin/users/+page.svelte` -- [ ] T025 [US2] Update Navigation Bar to hide links and show user profile/logout using `src/lib/ui` in `frontend/src/components/Navbar.svelte` -- [ ] T042 [US2] Implement `PermissionGuard` frontend component for granular UI element protection in `frontend/src/components/auth/PermissionGuard.svelte` -- [ ] T045 [US2] Implement multi-role permission resolution logic (union of permissions) in `backend/src/services/auth_service.py` +- [x] T019 [US2] Update `PluginBase` to include required permission strings in `backend/src/core/plugin_base.py` +- [x] T020 [US2] Implement `has_permission` dependency for route protection in `backend/src/dependencies.py` +- [x] T021 [US2] Protect existing plugin API routes using `has_permission` in `backend/src/api/routes/*.py` +- [x] T022 [US2] Implement `SystemAdminPlugin` inheriting from `PluginBase` for User/Role management in `backend/src/plugins/system_admin.py` +- [x] T023 [US2] Implement Admin API endpoints within `SystemAdminPlugin` in `backend/src/api/routes/admin.py` +- [ ] T053 [US2] Extend Admin API with User Update/Delete and Role CRUD endpoints in `backend/src/api/routes/admin.py` +- [ ] T054 [US2] Add Pydantic schemas for UserUpdate, RoleCreate, RoleUpdate in `backend/src/schemas/auth.py` +- [x] T051 [US2] Implement `adminService.js` for frontend API orchestration +- [ ] T055 [US2] Update `adminService.js` with new CRUD methods +- [x] T024 [US2] Create Admin Dashboard UI using `src/lib/ui` and `src/lib/i18n` in `frontend/src/routes/admin/users/+page.svelte` +- [ ] T056 [US2] Update Admin User Dashboard to support Edit/Delete operations in `frontend/src/routes/admin/users/+page.svelte` +- [ ] T057 [US4] Create Role Management UI in `frontend/src/routes/admin/roles/+page.svelte` +- [x] T025 [US2] Update Navigation Bar to hide links and show user profile/logout using `src/lib/ui` in `frontend/src/components/Navbar.svelte` +- [x] T042 [US2] Implement `PermissionGuard` frontend component for granular UI element protection in `frontend/src/components/auth/PermissionGuard.svelte` +- [x] T045 [US2] Implement multi-role permission resolution logic (union of permissions) in `backend/src/services/auth_service.py` ## Phase 4: User Story 3 - ADFS Integration (Priority: P2) *Goal: Enable corporate SSO login via ADFS and JIT provisioning.* -- [ ] T026 [US3] Configure Authlib for ADFS OIDC in `backend/src/core/auth/oauth.py` -- [ ] T027 [US3] Create `ADGroupMapping` model in `backend/src/models/auth.py` and update DB init script -- [ ] T028 [US3] Implement JIT provisioning logic (create user if maps to group) in `backend/src/services/auth_service.py` -- [ ] T029 [US3] Create API endpoints `GET /api/auth/login/adfs` and `GET /api/auth/callback/adfs` in `backend/src/api/auth.py` -- [ ] T030 [US3] Update Login Page to include "Login with ADFS" button using `src/lib/ui` in `frontend/src/routes/login/+page.svelte` -- [ ] T031 [US3] Implement Admin UI for configuring AD Group Mappings in `frontend/src/routes/admin/settings/+page.svelte` -- [ ] T041 [US3] Create ADFS mock provider for local testing and CI in `backend/tests/auth/mock_adfs.py` -- [ ] T046 [US3] Implement token refresh logic for ADFS OIDC tokens in `backend/src/core/auth/jwt.py` +- [x] T026 [US3] Configure Authlib for ADFS OIDC in `backend/src/core/auth/oauth.py` +- [x] T027 [US3] Create `ADGroupMapping` model in `backend/src/models/auth.py` and update DB init script +- [x] T028 [US3] Implement JIT provisioning logic (create user if maps to group) in `backend/src/services/auth_service.py` +- [x] T029 [US3] Create API endpoints `GET /api/auth/login/adfs` and `GET /api/auth/callback/adfs` in `backend/src/api/auth.py` +- [x] T030 [US3] Update Login Page to include "Login with ADFS" button using `src/lib/ui` in `frontend/src/routes/login/+page.svelte` +- [x] T031 [US3] Implement Admin UI for configuring AD Group Mappings in `frontend/src/routes/admin/settings/+page.svelte` +- [x] T052 [US3] Extend Admin API with AD mapping endpoints in `backend/src/api/routes/admin.py` +- [x] T041 [US3] Create ADFS mock provider for local testing and CI in `backend/tests/auth/mock_adfs.py` +- [x] T046 [US3] Implement token refresh logic for ADFS OIDC tokens in `backend/src/core/auth/jwt.py` ## Phase 5: Polish & Security Hardening *Goal: Ensure security best practices and smooth UX.* -- [ ] T032 Ensure all cookies are set with `HttpOnly` and `Secure` flags in `backend/src/api/auth.py` -- [ ] T033 Implement rate limiting and account lockout policy in `backend/src/api/auth.py` -- [ ] T034 Verify error messages are generic (no username enumeration) across all auth endpoints -- [ ] T035 Add "Session Expired" handling in frontend interceptor in `frontend/src/lib/api/client.ts` -- [ ] T036 Final manual test of switching between Local and ADFS login flows -- [ ] T040 Add confirmation dialogs for destructive admin actions using `src/lib/ui` in `frontend/src/routes/admin/users/+page.svelte` -- [ ] T047 Implement audit logging for security events (login, logout, permission changes) in `backend/src/core/auth/logger.py` -- [ ] T048 Perform UI accessibility audit (keyboard nav, ARIA alerts) for all auth components -- [ ] T049 Implement unit and integration tests for Local Auth and RBAC in `backend/tests/auth/` -- [ ] T050 Implement E2E tests for ADFS flow using mock provider in `tests/e2e/auth.spec.ts` +- [x] T032 Ensure all cookies are set with `HttpOnly` and `Secure` flags in `backend/src/api/auth.py` +- [x] T033 Implement rate limiting and account lockout policy in `backend/src/api/auth.py` +- [x] T034 Verify error messages are generic (no username enumeration) across all auth endpoints +- [x] T035 Add "Session Expired" handling in frontend interceptor in `frontend/src/lib/api/client.ts` +- [x] T036 Final manual test of switching between Local and ADFS login flows +- [x] T040 Add confirmation dialogs for destructive admin actions using `src/lib/ui` in `frontend/src/routes/admin/users/+page.svelte` +- [x] T047 Implement audit logging for security events (login, logout, permission changes) in `backend/src/core/auth/logger.py` +- [x] T048 Perform UI accessibility audit (keyboard nav, ARIA alerts) for all auth components +- [x] T049 Implement unit and integration tests for Local Auth and RBAC in `backend/tests/auth/` +- [x] T050 Implement E2E tests for ADFS flow using mock provider in `tests/e2e/auth.spec.ts` ## Dependencies