@cryptotaxi247 / CoPilot / commits / bf1cc35f

554 copilot analyst user (#558)

* feat: implement access denied page and update error handling for permissions * refactor: clean up formatting and improve function declarations in AccessDenied component

taylor_socfortress committed Dec 11, 2025 at 08:43 UTC bf1cc35f1c0782582be72caf433dcd6ab57aa007
4 files changed +119 -40
backend/app/auth/utils.py
+4 -4
@@ -233,8 +233,8 @@ class AuthHandler:
233 for scope in security_scopes.scopes:
234 if scope not in token_scopes:
235 raise HTTPException(
236 - status_code=401,
237 - detail="Not enough permissions",
236 + status_code=403,
237 + detail=f"Insufficient permissions. Required scope: {scope}",
238 headers={"WWW-Authenticate": authenticate_value},
239 )
240
@@ -289,8 +289,8 @@ class AuthHandler:
289
290 if not any(scope in token_scopes for scope in required_scopes):
291 raise HTTPException(
292 - status_code=401,
293 - detail="Not enough permissions, you don't have any of the required scopes.",
292 + status_code=403,
293 + detail=f"Insufficient permissions. Required one of: {', '.join(required_scopes)}",
294 headers={"WWW-Authenticate": "Bearer"},
295 )
296
customer_portal/src/api/httpClient.ts
+47 -36
@@ -4,51 +4,62 @@ import { useAuthStore } from "@/stores/auth"
4 import { isDebounceTimeOver, isJwtExpiring } from "@/utils/auth"
5
6 const HttpClient = axios.create({
7 - baseURL: "/api"
7 + baseURL: "/api"
8 })
9
10 let __TOKEN_REFRESHING = false
11 let __TOKEN_LAST_CHECK: Date | null = null
12
13 HttpClient.interceptors.request.use(
14 - config => {
15 - const store = useAuthStore()
16 -
17 - if (!config.headers) config.headers = {} as AxiosRequestHeaders
18 - if (store.userToken) {
19 - config.headers.Authorization = `Bearer ${store.userToken}`
20 - }
21 -
22 - if (
23 - store.userToken &&
24 - isJwtExpiring(store.userToken, 60 * 60) &&
25 - !__TOKEN_REFRESHING &&
26 - isDebounceTimeOver(__TOKEN_LAST_CHECK)
27 - ) {
28 - __TOKEN_REFRESHING = true
29 - __TOKEN_LAST_CHECK = new Date()
30 -
31 - store.refreshToken().then(() => {
32 - __TOKEN_REFRESHING = false
33 - })
34 - }
35 -
36 - return config
37 - },
38 - error => Promise.reject(error)
14 + config => {
15 + const store = useAuthStore()
16 +
17 + if (!config.headers) config.headers = {} as AxiosRequestHeaders
18 + if (store.userToken) {
19 + config.headers.Authorization = `Bearer ${store.userToken}`
20 + }
21 +
22 + if (
23 + store.userToken &&
24 + isJwtExpiring(store.userToken, 60 * 60) &&
25 + !__TOKEN_REFRESHING &&
26 + isDebounceTimeOver(__TOKEN_LAST_CHECK)
27 + ) {
28 + __TOKEN_REFRESHING = true
29 + __TOKEN_LAST_CHECK = new Date()
30 +
31 + store.refreshToken().then(() => {
32 + __TOKEN_REFRESHING = false
33 + })
34 + }
35 +
36 + return config
37 + },
38 + error => Promise.reject(error)
39 )
40
41 HttpClient.interceptors.response.use(
42 - response => response,
43 - error => {
44 - if (error.response && error.response.status === 401) {
45 - if (!window.location.pathname.includes("login")) {
46 - window.location.href = "/logout"
47 - }
48 - }
49 -
50 - return Promise.reject(error)
51 - }
42 + response => response,
43 + error => {
44 + if (error.response) {
45 + const status = error.response.status
46 +
47 + if (status === 401) {
48 + // Unauthorized - authentication failed (invalid/expired token)
49 + if (!window.location.pathname.includes("login")) {
50 + window.location.href = "/logout"
51 + }
52 + } else if (status === 403) {
53 + // Forbidden - insufficient permissions (keep user logged in)
54 + if (!window.location.pathname.includes("access-denied")) {
55 + const message = error.response.data?.detail || "You do not have permission to access this resource."
56 + window.location.href = `/access-denied?message=${encodeURIComponent(message)}`
57 + }
58 + }
59 + }
60 +
61 + return Promise.reject(error)
62 + }
63 )
64
65 export { HttpClient }
customer_portal/src/router/index.ts
+7
@@ -5,6 +5,7 @@ import AlertsPage from "@/views/AlertsPage.vue"
5 import CasesPage from "@/views/CasesPage.vue"
6 import CaseDetailsView from "@/views/CaseDetailsView.vue"
7 import AgentsPage from "@/views/AgentsPage.vue"
8 +import AccessDenied from '@/components/common/AccessDenied.vue'
9
10 const NotFound = {
11 template: `
@@ -61,6 +62,12 @@ const routes = [
62 component: AgentsPage,
63 meta: { requiresAuth: true }
64 },
65 + {
66 + path: '/access-denied',
67 + name: 'AccessDenied',
68 + component: AccessDenied,
69 + meta: { requiresAuth: false }
70 + },
71 {
72 path: "/:pathMatch(.*)*",
73 name: "NotFound",
frontend/src/components/common/AccessDenied.vue new
+61
@@ -0,0 +1,61 @@
1 +<!-- filepath: /Users/taylor/Desktop/Repos/CoPilot/frontend/src/components/common/AccessDenied.vue -->
2 +<template>
3 + <div class="access-denied-container">
4 + <n-result
5 + status="403"
6 + title="Access Denied"
7 + :description="description || 'You do not have permission to view this page.'"
8 + >
9 + <template #footer>
10 + <n-space justify="center">
11 + <n-button type="primary" @click="goBack">
12 + <template #icon>
13 + <n-icon>
14 + <ArrowBackOutline />
15 + </n-icon>
16 + </template>
17 + Go Back
18 + </n-button>
19 + <n-button @click="goHome">
20 + <template #icon>
21 + <n-icon>
22 + <HomeOutline />
23 + </n-icon>
24 + </template>
25 + Go to Dashboard
26 + </n-button>
27 + </n-space>
28 + </template>
29 + </n-result>
30 + </div>
31 +</template>
32 +
33 +<script setup lang="ts">
34 +import { ArrowBackOutline, HomeOutline } from '@vicons/ionicons5'
35 +import { useRouter } from 'vue-router'
36 +
37 +interface Props {
38 + description?: string
39 +}
40 +
41 +defineProps<Props>()
42 +
43 +const router = useRouter()
44 +
45 +function goBack() {
46 + router.back()
47 +}
48 +
49 +function goHome() {
50 + router.push('/')
51 +}
52 +</script>
53 +
54 +<style scoped>
55 +.access-denied-container {
56 + display: flex;
57 + align-items: center;
58 + justify-content: center;
59 + min-height: 60vh;
60 +}
61 +</style>