@innertia-solutions/nuxt-app 0.1.0

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.
@@ -0,0 +1,60 @@
1
+ // useRequestInterceptors auto-imported from nuxt-core
2
+ // useAuthStore auto-imported from this package
3
+
4
+ export function useApi() {
5
+ const config = useRuntimeConfig()
6
+ const baseUrl = config.public.apiBaseUrl || '/api'
7
+ const loginPath = config.public.loginPath || '/login'
8
+
9
+ const { run, add } = useRequestInterceptors()
10
+
11
+ async function makeRequest(method, path, body = null, options = {}) {
12
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }
13
+ run(headers, options)
14
+
15
+ const cleanPath = path.startsWith('/') ? path.slice(1) : path
16
+ const url = `${baseUrl}/${cleanPath}`
17
+
18
+ const fetchOptions = { method, headers }
19
+ if (body !== null) fetchOptions.body = JSON.stringify(body)
20
+
21
+ const response = await fetch(url, fetchOptions)
22
+
23
+ if (response.status === 401) {
24
+ const authStore = useAuthStore()
25
+ authStore.logout()
26
+ await navigateTo(loginPath)
27
+ return null
28
+ }
29
+
30
+ const contentType = response.headers.get('content-type') ?? ''
31
+ const data = contentType.includes('application/json') ? await response.json() : await response.text()
32
+
33
+ if (!response.ok) {
34
+ const err = new Error(`API error ${response.status}`)
35
+ err.status = response.status
36
+ err.data = data
37
+ throw err
38
+ }
39
+
40
+ return data
41
+ }
42
+
43
+ const get = (path, options = {}) => makeRequest('GET', path, null, options)
44
+ const post = (path, body, options = {}) => makeRequest('POST', path, body, options)
45
+ const put = (path, body, options = {}) => makeRequest('PUT', path, body, options)
46
+ const patch = (path, body, options = {}) => makeRequest('PATCH', path, body, options)
47
+ const del = (path, options = {}) => makeRequest('DELETE', path, null, options)
48
+
49
+ // *Sync aliases — same methods, named for clarity in call sites
50
+ const getSync = get
51
+ const postSync = post
52
+ const putSync = put
53
+ const patchSync = patch
54
+ const deleteSync = del
55
+
56
+ /** Shortcut to add an interceptor from a composable or plugin */
57
+ const addInterceptor = (fn) => add(fn)
58
+
59
+ return { get, post, put, patch, delete: del, getSync, postSync, putSync, patchSync, deleteSync, addInterceptor }
60
+ }
@@ -0,0 +1,68 @@
1
+ // useAuthStore, useApi auto-imported
2
+
3
+ export function useAuth() {
4
+ const authStore = useAuthStore()
5
+ const api = useApi()
6
+ const config = useRuntimeConfig()
7
+ const loginPath = config.public.loginPath || '/login'
8
+
9
+ /**
10
+ * Standard login (email + password).
11
+ * context: role/area slug used in the API path (e.g. 'admin', 'technician').
12
+ */
13
+ async function performLogin(context, email, password, remember = false) {
14
+ authStore.rememberUser = remember
15
+ const data = await api.post(`${context}/auth/login`, { email, password })
16
+ authStore.saveToken(data.access_token)
17
+ authStore.setCurrentContext(context)
18
+ await fetchMe()
19
+ return data
20
+ }
21
+
22
+ /**
23
+ * Load current user, permissions, and available contexts from the API.
24
+ * Called after login and after context switch.
25
+ */
26
+ async function fetchMe() {
27
+ const data = await api.get('auth/me')
28
+ authStore.saveUser(data.user)
29
+ authStore.savePermissions(data.permissions ?? [])
30
+ authStore.availableContexts = data.availableContexts ?? []
31
+ return data
32
+ }
33
+
34
+ /**
35
+ * Logout: best-effort POST to backend, then clear local state and redirect.
36
+ */
37
+ async function logout() {
38
+ try {
39
+ await api.post('auth/logout', {})
40
+ } catch {
41
+ // best-effort — ignore network failures
42
+ }
43
+ authStore.logout()
44
+ await navigateTo(loginPath)
45
+ }
46
+
47
+ /**
48
+ * Get the OAuth redirect URL for a provider.
49
+ * Returns the URL string from the backend.
50
+ */
51
+ async function getOauthRedirectUrl(context, provider) {
52
+ const data = await api.get(`${context}/auth/oauth/${provider}/redirect`)
53
+ return data.url
54
+ }
55
+
56
+ /**
57
+ * Handle OAuth callback. Same success path as performLogin.
58
+ */
59
+ async function handleOauthCallback(context, provider, code) {
60
+ const data = await api.post(`${context}/auth/oauth/${provider}/callback`, { code })
61
+ authStore.saveToken(data.access_token)
62
+ authStore.setCurrentContext(context)
63
+ await fetchMe()
64
+ return data
65
+ }
66
+
67
+ return { performLogin, fetchMe, logout, getOauthRedirectUrl, handleOauthCallback }
68
+ }
@@ -0,0 +1,44 @@
1
+ // useAuthStore, useApi, useAuth auto-imported
2
+ import { computed } from 'vue'
3
+
4
+ export function useContext() {
5
+ const authStore = useAuthStore()
6
+ const api = useApi()
7
+ const { fetchMe } = useAuth()
8
+
9
+ const currentContext = computed(() => authStore.currentContext)
10
+ const availableContexts = computed(() => authStore.availableContexts)
11
+
12
+ /**
13
+ * Check whether user has permission to switch to targetContext.
14
+ * Returns:
15
+ * { success: false, reason: 'no_permission' } — user cannot switch
16
+ * { success: true, requiresConfirmation: true } — show confirmation UI
17
+ */
18
+ async function switchContext(targetContext) {
19
+ const data = await api.get(`auth/context/${targetContext}/check`)
20
+ if (!data.hasAccess) {
21
+ return { success: false, reason: 'no_permission' }
22
+ }
23
+ return { success: true, requiresConfirmation: true }
24
+ }
25
+
26
+ /**
27
+ * Execute the context switch after user confirmation.
28
+ * Updates store and reloads permissions via fetchMe.
29
+ */
30
+ async function confirmSwitch(targetContext) {
31
+ authStore.setCurrentContext(targetContext)
32
+ await fetchMe()
33
+ return { success: true }
34
+ }
35
+
36
+ /**
37
+ * Quick synchronous check — is this context in the available list?
38
+ */
39
+ function hasAccessToContext(context) {
40
+ return authStore.availableContexts.includes(context)
41
+ }
42
+
43
+ return { currentContext, availableContexts, switchContext, confirmSwitch, hasAccessToContext }
44
+ }
@@ -0,0 +1,19 @@
1
+ // useAuthStore auto-imported
2
+
3
+ export function usePermissions() {
4
+ const authStore = useAuthStore()
5
+
6
+ /** Check a single permission string */
7
+ const can = (permission) => authStore.permissions.includes(permission)
8
+
9
+ /** Check a single role string */
10
+ const hasRole = (role) => authStore.user?.roles?.includes(role) ?? false
11
+
12
+ /** True if user has at least one of the given permissions */
13
+ const hasAny = (permissions) => permissions.some(p => authStore.permissions.includes(p))
14
+
15
+ /** True if user has all of the given permissions */
16
+ const hasAll = (permissions) => permissions.every(p => authStore.permissions.includes(p))
17
+
18
+ return { can, hasRole, hasAny, hasAll }
19
+ }
@@ -0,0 +1,9 @@
1
+ // Redirect unauthenticated users to login.
2
+ // useAuthStore auto-imported from nuxt-app stores.
3
+ export default defineNuxtRouteMiddleware(() => {
4
+ const authStore = useAuthStore()
5
+ const config = useRuntimeConfig()
6
+ if (!authStore.isAuthenticated()) {
7
+ return navigateTo(config.public.loginPath || '/login')
8
+ }
9
+ })
@@ -0,0 +1,9 @@
1
+ // Redirect already-authenticated users away from guest-only pages (login, register).
2
+ // useAuthStore auto-imported from nuxt-app stores.
3
+ export default defineNuxtRouteMiddleware(() => {
4
+ const authStore = useAuthStore()
5
+ const config = useRuntimeConfig()
6
+ if (authStore.isAuthenticated()) {
7
+ return navigateTo(config.public.homePath || '/')
8
+ }
9
+ })
package/nuxt.config.ts ADDED
@@ -0,0 +1,4 @@
1
+ export default defineNuxtConfig({
2
+ extends: ['@innertia-solutions/nuxt-core'],
3
+ imports: { dirs: ['stores', 'composables'] },
4
+ })
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@innertia-solutions/nuxt-app",
3
+ "version": "0.1.0",
4
+ "description": "Innertia Solutions — Nuxt app layer: auth, context, permissions, API client",
5
+ "keywords": ["nuxt", "vue", "pinia", "auth", "permissions", "context"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/innertia-solutions/innertia-nuxt"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "main": "./nuxt.config.ts",
15
+ "exports": {
16
+ ".": "./nuxt.config.ts"
17
+ },
18
+ "peerDependencies": {
19
+ "nuxt": ">=4.0.0",
20
+ "vue": ">=3.5.0"
21
+ },
22
+ "dependencies": {
23
+ "@innertia-solutions/nuxt-core": "^0.1.3"
24
+ },
25
+ "devDependencies": {
26
+ "nuxt": "^4.4.2",
27
+ "vue": "^3.5.0"
28
+ }
29
+ }
@@ -0,0 +1,13 @@
1
+ // Registers the Authorization header interceptor into the shared registry.
2
+ // Runs only on the client (cookie-based token is not available on the server).
3
+ export default defineNuxtPlugin(() => {
4
+ const { add } = useRequestInterceptors()
5
+ const authStore = useAuthStore()
6
+
7
+ add((headers: Record<string, string>, options: Record<string, unknown>) => {
8
+ if (options.useToken !== false) {
9
+ const token = authStore.getToken()
10
+ if (token) headers['Authorization'] = `Bearer ${token}`
11
+ }
12
+ })
13
+ })
@@ -0,0 +1,8 @@
1
+ // On app boot: if the stored token exists but is expired, clear auth state silently.
2
+ // This prevents stale tokens from reaching the API.
3
+ export default defineNuxtPlugin(() => {
4
+ const authStore = useAuthStore()
5
+ if (authStore.token && !authStore.isAuthenticated()) {
6
+ authStore.logout()
7
+ }
8
+ })
package/stores/auth.js ADDED
@@ -0,0 +1,116 @@
1
+ import { defineStore } from 'pinia'
2
+
3
+ // ─── cookie helpers ────────────────────────────────────────────────────────
4
+ // Never call useCookie() inside Pinia actions — it throws outside Vue setup.
5
+ // Pattern: try useCookie (works in setup), catch → document.cookie fallback.
6
+
7
+ function _setCookie(name, value, maxAgeSeconds) {
8
+ try {
9
+ const isDev = import.meta.env?.DEV ?? false
10
+ const cookie = useCookie(name, { maxAge: maxAgeSeconds, sameSite: 'lax', secure: !isDev })
11
+ cookie.value = typeof value === 'object' ? JSON.stringify(value) : value
12
+ } catch {
13
+ if (import.meta.client) {
14
+ const expires = maxAgeSeconds ? `; Max-Age=${Math.floor(maxAgeSeconds)}` : ''
15
+ const val = typeof value === 'object' ? JSON.stringify(value) : value
16
+ document.cookie = `${name}=${encodeURIComponent(val)}${expires}; path=/; SameSite=Lax`
17
+ }
18
+ }
19
+ }
20
+
21
+ function _getCookie(name) {
22
+ try {
23
+ const cookie = useCookie(name)
24
+ return cookie.value ?? null
25
+ } catch {
26
+ if (import.meta.client) {
27
+ const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'))
28
+ return match ? decodeURIComponent(match[1]) : null
29
+ }
30
+ return null
31
+ }
32
+ }
33
+
34
+ function _deleteCookie(name) {
35
+ try {
36
+ const cookie = useCookie(name)
37
+ cookie.value = null
38
+ } catch {
39
+ if (import.meta.client) {
40
+ document.cookie = `${name}=; Max-Age=0; path=/`
41
+ }
42
+ }
43
+ }
44
+
45
+ // ─── JWT decode (no lib) ───────────────────────────────────────────────────
46
+ function _decodeJwtExpiry(token) {
47
+ try {
48
+ const payload = JSON.parse(atob(token.split('.')[1]))
49
+ return payload.exp ?? null
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
55
+ // ─── store ─────────────────────────────────────────────────────────────────
56
+ export const useAuthStore = defineStore('auth', {
57
+ state: () => ({
58
+ token: null,
59
+ user: null,
60
+ currentContext: null,
61
+ availableContexts: [],
62
+ permissions: [],
63
+ rememberUser: false,
64
+ }),
65
+
66
+ persist: {
67
+ pick: ['token', 'user', 'currentContext', 'availableContexts'],
68
+ },
69
+
70
+ actions: {
71
+ // ── token ──────────────────────────────────────────────────────────────
72
+ saveToken(token) {
73
+ this.token = token
74
+ _setCookie('auth_token', token, 60 * 60 * 24 * 7) // 7 days
75
+ },
76
+
77
+ getToken() {
78
+ return this.token ?? _getCookie('auth_token')
79
+ },
80
+
81
+ isAuthenticated() {
82
+ const token = this.getToken()
83
+ if (!token) return false
84
+ const exp = _decodeJwtExpiry(token)
85
+ if (exp === null) return true // non-JWT or no expiry claim → treat as valid
86
+ return Date.now() / 1000 < exp
87
+ },
88
+
89
+ // ── user ───────────────────────────────────────────────────────────────
90
+ saveUser(user) {
91
+ this.user = user
92
+ _setCookie('auth_user', user, 60 * 60 * 24 * 7)
93
+ },
94
+
95
+ // ── context ────────────────────────────────────────────────────────────
96
+ setCurrentContext(context) {
97
+ this.currentContext = context
98
+ },
99
+
100
+ // ── permissions ────────────────────────────────────────────────────────
101
+ savePermissions(permissions) {
102
+ this.permissions = permissions ?? []
103
+ },
104
+
105
+ // ── logout ─────────────────────────────────────────────────────────────
106
+ logout() {
107
+ this.token = null
108
+ this.user = null
109
+ this.currentContext = null
110
+ this.availableContexts = []
111
+ this.permissions = []
112
+ _deleteCookie('auth_token')
113
+ _deleteCookie('auth_user')
114
+ },
115
+ },
116
+ })