+
diff --git a/app/components/pub/form/label.vue b/app/components/pub/form/label.vue
new file mode 100644
index 00000000..b1ad2d55
--- /dev/null
+++ b/app/components/pub/form/label.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/nav/data-table.vue b/app/components/pub/nav/data-table.vue
new file mode 100644
index 00000000..9dc72099
--- /dev/null
+++ b/app/components/pub/nav/data-table.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+ {{ h.label }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ funcParsed[key]?.(row) ?? funcHtml[key]?.(row) ?? row[key] }}
+
+
+
+
+
+
diff --git a/app/components/pub/nav/dropdown-action-du.vue b/app/components/pub/nav/dropdown-action-du.vue
new file mode 100644
index 00000000..52c999fb
--- /dev/null
+++ b/app/components/pub/nav/dropdown-action-du.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/nav/dropdown-action-ducd.vue b/app/components/pub/nav/dropdown-action-ducd.vue
new file mode 100644
index 00000000..a4073a93
--- /dev/null
+++ b/app/components/pub/nav/dropdown-action-ducd.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/nav/dropdown-action-dud.vue b/app/components/pub/nav/dropdown-action-dud.vue
new file mode 100644
index 00000000..5a9c30a9
--- /dev/null
+++ b/app/components/pub/nav/dropdown-action-dud.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/nav/dropdown-action-ud.vue b/app/components/pub/nav/dropdown-action-ud.vue
new file mode 100644
index 00000000..0ef020d7
--- /dev/null
+++ b/app/components/pub/nav/dropdown-action-ud.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/nav/footer/cs.vue b/app/components/pub/nav/footer/cs.vue
index 96725a05..d51bf74a 100644
--- a/app/components/pub/nav/footer/cs.vue
+++ b/app/components/pub/nav/footer/cs.vue
@@ -1,12 +1,24 @@
-
+
-
-
-
-
- Edit
-
-
+
+
+
+ Kembali
+
+
+
+ Selesai
+
diff --git a/app/components/pub/nav/footer/csd.vue b/app/components/pub/nav/footer/csd.vue
new file mode 100644
index 00000000..78874932
--- /dev/null
+++ b/app/components/pub/nav/footer/csd.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Kembali
+
+
+
+ Draf
+
+
+
+ Selesai
+
+
+
diff --git a/app/components/pub/nav/header/prep.vue b/app/components/pub/nav/header/prep.vue
index 8d856333..ab37900d 100644
--- a/app/components/pub/nav/header/prep.vue
+++ b/app/components/pub/nav/header/prep.vue
@@ -32,7 +32,7 @@ function btnClick() {
diff --git a/app/components/pub/nav/types.ts b/app/components/pub/nav/types.ts
index 91b53410..aa4d130b 100644
--- a/app/components/pub/nav/types.ts
+++ b/app/components/pub/nav/types.ts
@@ -82,3 +82,12 @@ export interface KeyNames {
key: string
label: string
}
+
+export interface LinkItem {
+ label: string
+ icon?: string
+ href?: string // to cover the needs of stating full external origins full url
+ action?: string // for local paths
+ onClick?: (event: Event) => void
+ headerStatus?: boolean
+}
diff --git a/app/components/pub/ui/input/Input.vue b/app/components/pub/ui/input/Input.vue
index 81140b40..2c7e0141 100644
--- a/app/components/pub/ui/input/Input.vue
+++ b/app/components/pub/ui/input/Input.vue
@@ -1,7 +1,7 @@
-
+
diff --git a/app/composables/useRBAC.ts b/app/composables/useRBAC.ts
new file mode 100644
index 00000000..6228f37f
--- /dev/null
+++ b/app/composables/useRBAC.ts
@@ -0,0 +1,48 @@
+import type { Permission, RoleAccess } from '~/models/role'
+
+/**
+ * Check if user has access to a page
+ */
+export function useRBAC() {
+ // NOTE: this roles was dummy for testing only, it should taken from the user store
+ // const authStore = useAuthStore()
+
+ const checkRole = (roleAccess: RoleAccess, _userRoles?: string[]): boolean => {
+ const roles = ['admisi']
+ return roles.some((role: string) => role in roleAccess)
+ }
+
+ const checkPermission = (roleAccess: RoleAccess, permission: Permission, _userRoles?: string[]): boolean => {
+ const roles = ['admisi']
+ return roles.some((role: string) => roleAccess[role]?.includes(permission))
+ }
+
+ const getUserPermissions = (roleAccess: RoleAccess, _userRoles?: string[]): Permission[] => {
+ // const roles = userRoles || authStore.roles
+ const roles = ['admisi']
+ const permissions = new Set
()
+
+ roles.forEach((role) => {
+ if (roleAccess[role]) {
+ roleAccess[role].forEach((permission) => permissions.add(permission))
+ }
+ })
+
+ return Array.from(permissions)
+ }
+
+ const hasCreateAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'C')
+ const hasReadAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'R')
+ const hasUpdateAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'U')
+ const hasDeleteAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'D')
+
+ return {
+ checkRole,
+ checkPermission,
+ getUserPermissions,
+ hasCreateAccess,
+ hasReadAccess,
+ hasUpdateAccess,
+ hasDeleteAccess,
+ }
+}
diff --git a/app/error.vue b/app/error.vue
new file mode 100644
index 00000000..27c04cc0
--- /dev/null
+++ b/app/error.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/app/layouts/default.vue b/app/layouts/default.vue
index d63792ae..6e6c32ce 100644
--- a/app/layouts/default.vue
+++ b/app/layouts/default.vue
@@ -1,4 +1,21 @@
-
+
@@ -6,10 +23,70 @@
-
+
diff --git a/app/lib/page-permission.ts b/app/lib/page-permission.ts
new file mode 100644
index 00000000..797dd077
--- /dev/null
+++ b/app/lib/page-permission.ts
@@ -0,0 +1,12 @@
+import type { RoleAccess } from '~/models/role'
+
+export const PAGE_PERMISSIONS = {
+ '/patient': {
+ doctor: ['R'],
+ nurse: ['R'],
+ admisi: ['C', 'R', 'U', 'D'],
+ pharmacy: ['R'],
+ billing: ['R'],
+ management: ['R'],
+ },
+} as const satisfies Record
diff --git a/app/middleware/auth.global.ts b/app/middleware/auth.global.ts
index e3b98900..e2b76d91 100644
--- a/app/middleware/auth.global.ts
+++ b/app/middleware/auth.global.ts
@@ -1,4 +1,6 @@
export default defineNuxtRouteMiddleware((to) => {
+ if (to.meta.public) return
+
const { $pinia } = useNuxtApp()
if (import.meta.client) {
@@ -7,12 +9,7 @@ export default defineNuxtRouteMiddleware((to) => {
console.log('currRole', userStore.userRole)
console.log('isAuth', userStore.isAuthenticated)
if (!userStore.isAuthenticated) {
- return navigateTo('/auth/login')
- }
-
- const allowedRoles = to.meta.roles as string[] | undefined
- if (allowedRoles && !allowedRoles.includes(userStore.userRole)) {
- return navigateTo('/unauthorized')
+ return navigateTo('/401')
}
}
})
diff --git a/app/middleware/rbac.ts b/app/middleware/rbac.ts
new file mode 100644
index 00000000..8528c2ca
--- /dev/null
+++ b/app/middleware/rbac.ts
@@ -0,0 +1,31 @@
+import { PAGE_PERMISSIONS } from '~/lib/page-permission'
+
+export default defineNuxtRouteMiddleware((to) => {
+ if (to.meta.public) return
+
+ const { $pinia } = useNuxtApp()
+ if (import.meta.server) {
+ const authStore = useUserStore($pinia)
+ // Check specific page permissions if defined in config
+ const pagePermissions = PAGE_PERMISSIONS[to.path as keyof typeof PAGE_PERMISSIONS]
+ if (pagePermissions) {
+ const { checkRole } = useRBAC()
+ if (!checkRole(pagePermissions)) {
+ return navigateTo('/403')
+ }
+ }
+
+ // Fallback to meta roles
+ const requiredRoles = to.meta.roles as string[]
+ if (requiredRoles && requiredRoles.length > 0) {
+ // FIXME: change this dummy roles, when api is ready
+ // const userRoles = authStore.roles
+ const userRoles = ['admisi']
+ const hasRequiredRole = requiredRoles.some((role) => userRoles.includes(role))
+
+ if (!hasRequiredRole) {
+ return navigateTo('/403')
+ }
+ }
+ }
+})
diff --git a/app/models/role.ts b/app/models/role.ts
new file mode 100644
index 00000000..6c6097a9
--- /dev/null
+++ b/app/models/role.ts
@@ -0,0 +1,22 @@
+import type { PAGE_PERMISSIONS } from '~/lib/page-permission'
+
+export interface User {
+ id: string
+ name: string
+ email: string
+}
+
+export interface AuthState {
+ user: User | null
+ roles: string[]
+ token: string | null
+}
+
+export type Permission = 'C' | 'R' | 'U' | 'D'
+
+export interface RoleAccess {
+ [role: string]: Permission[]
+}
+
+export type PagePath = keyof typeof PAGE_PERMISSIONS
+export type PagePermission = (typeof PAGE_PERMISSIONS)[PagePath]
diff --git a/app/pages/(error)/401.vue b/app/pages/(error)/401.vue
new file mode 100644
index 00000000..b60f313f
--- /dev/null
+++ b/app/pages/(error)/401.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
401
+
Unauthorized Access
+
+ Please log in with the appropriate credentials
+ to access this resource.
+
+
+ Login
+ Back to Home
+
+
+
+
+
+
diff --git a/app/pages/(error)/403.vue b/app/pages/(error)/403.vue
new file mode 100644
index 00000000..985bc412
--- /dev/null
+++ b/app/pages/(error)/403.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ 403
+
+
Access Forbidden
+
+ You don't have necessary permission
+ to view this resource.
+
+
+
+ Go Back
+
+
+ Back to Home
+
+
+
+
+
+
+
diff --git a/app/pages/(error)/404.vue b/app/pages/(error)/404.vue
new file mode 100644
index 00000000..a857fca1
--- /dev/null
+++ b/app/pages/(error)/404.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ 404
+
+
Oops! Page Not Found!
+
+ It seems like the page you're looking for
+ does not exist or might have been removed.
+
+
+
+ Go Back
+
+
+ Back to Home
+
+
+
+
+
+
+
diff --git a/app/pages/(error)/500.vue b/app/pages/(error)/500.vue
new file mode 100644
index 00000000..7c6cb120
--- /dev/null
+++ b/app/pages/(error)/500.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ 500
+
+
Oops! Something went wrong :')
+
+ We apologize for the inconvenience. Please try again later.
+
+
+
+ Go Back
+
+
+ Back to Home
+
+
+
+
+
+
+
diff --git a/app/pages/(error)/503.vue b/app/pages/(error)/503.vue
new file mode 100644
index 00000000..ccd86c2c
--- /dev/null
+++ b/app/pages/(error)/503.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
503
+
Website is under maintenance!
+
+ The site is not available at the moment.
+ We'll be back online shortly.
+
+
+ Go Back
+ Back to Home
+
+
+
+
+
+
diff --git a/app/pages/(features)/patient/add.vue b/app/pages/(features)/patient/add.vue
index 12063ef7..30b25cf4 100644
--- a/app/pages/(features)/patient/add.vue
+++ b/app/pages/(features)/patient/add.vue
@@ -1,9 +1,34 @@
-
+
+
+
+
diff --git a/app/pages/(features)/patient/index.vue b/app/pages/(features)/patient/index.vue
index f1db5b53..13e0c451 100644
--- a/app/pages/(features)/patient/index.vue
+++ b/app/pages/(features)/patient/index.vue
@@ -1,11 +1,35 @@
-
+
+
+
+
+
You don't have permission to view patient records.
+
diff --git a/app/pages/auth/login.vue b/app/pages/auth/login.vue
index eb157c8c..b2e8f89a 100644
--- a/app/pages/auth/login.vue
+++ b/app/pages/auth/login.vue
@@ -1,6 +1,7 @@
diff --git a/eslint.config.js b/eslint.config.js
index e786c1c0..4879bee5 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -18,14 +18,14 @@ export default withNuxt(
{
rules: {
// Basic rules
- quotes: ['error', 'single', { avoidEscape: true }],
+ 'quotes': ['error', 'single', { avoidEscape: true }],
'style/no-trailing-spaces': ['error', { ignoreComments: true }],
'no-console': 'off',
// Relax strict formatting rules
'style/brace-style': 'off', // Allow inline if
- curly: ['error', 'multi-line'], // Only require braces for multi-line
+ 'curly': ['error', 'multi-line'], // Only require braces for multi-line
'style/arrow-parens': 'off',
// UnoCSS - make it warning instead of error, or disable completely
@@ -48,6 +48,6 @@ export default withNuxt(
rules: {
'style/no-trailing-spaces': 'off',
},
- }
- )
+ },
+ ),
)
diff --git a/package.json b/package.json
index a77d07a3..74a2234b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "nuxt-app",
- "private": true,
"type": "module",
+ "private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
diff --git a/server/api/[...req].ts b/server/api/[...req].ts
index 88e27d59..3fd45b16 100644
--- a/server/api/[...req].ts
+++ b/server/api/[...req].ts
@@ -23,7 +23,7 @@ export default defineEventHandler(async (event) => {
if (headers['content-type']) forwardHeaders.set('Content-Type', headers['content-type'])
forwardHeaders.set('Authorization', `Bearer ${bearer}`)
- let body: any = undefined
+ let body: any
if (['POST', 'PATCH'].includes(method!)) {
if (headers['content-type']?.includes('multipart/form-data')) {
body = await readBody(event)
diff --git a/uno.config.ts b/uno.config.ts
index 48e7c051..fb109dd6 100644
--- a/uno.config.ts
+++ b/uno.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig, presetWind, presetAttributify, presetIcons } from 'unocss'
+import { defineConfig, presetAttributify, presetIcons, presetWind } from 'unocss'
export default defineConfig({
presets: [