diff --git a/.env.example b/.env.example index c80e1391..50d17cb5 100644 --- a/.env.example +++ b/.env.example @@ -7,13 +7,9 @@ SSO_CONFIRM_URL = X_AP_CODE=rssa-sso X_AP_SECRET_KEY=sapiperah -NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID=portal-simrs-new -# Local -NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET= -NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL=http://127.0.0.1:8080/realms/rssa_testing - KEYCLOAK_LOGOUT_REDIRECT=http://localhost:3000/ # test local KEYCLOAK_REALM=rssa_testing KEYCLOAK_URL=http://127.0.0.1:8080/ +CLIENT_ID=portal-simrs-new diff --git a/app/components/app/auth/login.vue b/app/components/app/auth/login.vue index 7131daf2..5acef75a 100644 --- a/app/components/app/auth/login.vue +++ b/app/components/app/auth/login.vue @@ -3,6 +3,8 @@ import type { z } from 'zod' import { toTypedSchema } from '@vee-validate/zod' import { Loader2 } from 'lucide-vue-next' import { useForm } from 'vee-validate' +import { useRouter } from 'vue-router' +import { useKeycloak } from "~/composables/useKeycloack" interface Props { schema: z.ZodSchema @@ -14,7 +16,6 @@ const props = defineProps() const emit = defineEmits<{ submit: [data: any] sso: [] - response: [state: string] }>() const { handleSubmit, defineField, errors, meta } = useForm({ @@ -36,24 +37,24 @@ const onSubmit = handleSubmit(async (values) => { } }) +const { initKeycloak, getProfile, login } = useKeycloak() +const profile = ref(null) + +onMounted(async () => { + await initKeycloak('check-sso') + profile.value = getProfile() + console.log(profile) +}) const onSSO = (async () => { - console.log("Emitting SSO...") try { - await emit('sso') + const redirect = window.location.origin + '/auth/sso' + await login({ redirectUri: redirect }) } catch (error) { console.error('Call SSO failed:', error) } }); -const test = useRoute() -const responseSSO = test.hash - -if (responseSSO != null && responseSSO != '') { - console.log("Getting Response SSO...") - await emit('response', responseSSO) -} - diff --git a/app/components/content/auth/login.vue b/app/components/content/auth/login.vue index 23ab2316..4efeef42 100644 --- a/app/components/content/auth/login.vue +++ b/app/components/content/auth/login.vue @@ -41,121 +41,10 @@ async function onSubmit(data: LoginFormData) { isLoading.value = false } - -const config = useRuntimeConfig() -// const store = useKeycloak() - -const state = reactive({ - loggedIn: false -}) - -async function onSSO() { - console.log("=================== on SSO! ===================") - const config = useRuntimeConfig() - - const keycloak = new Keycloak({ - url: config.public.KEYCLOAK_URL, - realm: config.public.KEYCLOAK_REALM, - clientId: config.public.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID, - }); - - try { - const authenticatedResult = await keycloak.init({ onLoad: 'login-required' }); // Or 'login-required' - // seelah line ini aku mek paham logic e tapi faktane dunno - const nuxtApp = useNuxtApp() - nuxtApp.provide('keycloak', keycloak); - - } catch (error) { - console.error('Keycloak initialization failed:', error); - } - - // const initOptions = { - // url: config.public.KEYCLOAK_URL, - // realm: config.public.KEYCLOAK_REALM, - // clientId: config.public.KEYCLOAK_ID, - // onLoad: 'login-required' - // } - - // const keycloak = new Keycloak(initOptions) - // console.log("=================== balik gak se! ===================") - // keycloak - // .init({ onLoad: initOptions.onLoad }) - // .then((auth) => { - // console.log(auth) - // if (!auth) { - // window.location.reload() - // } else { - // // store.setup(keycloak) - // state.loggedIn = true - // } - // }) - - console.log("=================== onto login fes! ===================") - // if (state.loggedIn) { - // const result = await xfetch('/api/v1/authentication/login-fes', 'POST', { - // data: keycloak, - // }) - - // if (result.success) { - // const { data: rawdata, meta } = result.body - // if (meta.status === 'verified') { - // login(rawdata) - // navigateTo('/') - // } - // } else { - // if (result.errors) { - // Object.entries(result.errors).forEach( - // ([field, errorInfo]: [string, any]) => (apiErrors.value[field] = errorInfo.message), - // ) - // } else { - // apiErrors.value.general = result.error?.message || result.message || 'Login failed' - // } - // } - // } - - // const urlSSO = - // config.public.KEYCLOAK_ISSUER + - // '/protocol/openid-connect/auth?client_id=' + - // config.public.KEYCLOAK_ID + - // '&scope=openid%20email%20profile&response_type=code&redirect_uri=' + - // config.public.KEYCLOAK_LOGOUT_REDIRECT + - // '%2Fapi%2Fauth%2Fcallback%2Fkeycloak&state=AKf-WHWdL822V3LaNS5MSFzJ96-VHp6FUXlXxIAzXXM&code_challenge=nXOcGLLlA1NtXI4RM4hL59iP_I_yQAsUDd5sAOkKBF4&code_challenge_method=S256' - // await navigateTo(urlSSO, - // { - // open: { target: '_blank' }, - // external: true - // }) -} - -async function onResponseSSO(authenticatedResult: string) { - console.log("=================== onto login fes!!! ===================") - console.log(authenticatedResult) - if (authenticatedResult) { - const result = await xfetch('/api/v1/authentication/login-fes', 'POST', { - data: authenticatedResult, - }) - - if (result.success) { - const { data: rawdata, meta } = result.body - if (meta.status === 'verified') { - login(rawdata) - navigateTo('/') - } - } else { - if (result.errors) { - Object.entries(result.errors).forEach( - ([field, errorInfo]: [string, any]) => (apiErrors.value[field] = errorInfo.message), - ) - } else { - apiErrors.value.general = result.error?.message || result.message || 'Login failed' - } - } - } -} diff --git a/app/composables/useKeycloack.ts b/app/composables/useKeycloack.ts new file mode 100644 index 00000000..a239322a --- /dev/null +++ b/app/composables/useKeycloack.ts @@ -0,0 +1,117 @@ +import Keycloak from "keycloak-js"; +import { ref, computed, onBeforeMount } from "vue"; + +let kc: any | null = null; + +const initialized = ref(false); +const authenticated = ref(false); +const token = ref(null); +const profile = ref(null); + +export function useKeycloak() { + const initKeycloak = async (onLoad: "login-required" | "check-sso" = "check-sso") => { + if (kc) return kc; + kc = new Keycloak({ + url: config.public.KEYCLOAK_URL, + realm: config.public.KEYCLOAK_REALM, + clientId: config.public.CLIENT_ID, + }); + + try { + const initOptions = { + onLoad, + promiseType: "native" as const, + pkceMethod: "S256" as const, + }; + console.log(kc.url) + authenticated.value = await kc.init(initOptions); + initialized.value = true; + token.value = kc.token ?? null; + if (authenticated.value) { + try { + profile.value = await kc.loadUserProfile(); + } catch (e) { + profile.value = null; + } + } + // automatically update token in background + kc.onTokenExpired = async () => { + try { + const refreshed = await kc.updateToken(30); + token.value = kc?.token ?? null; + if (!refreshed) { + // token not refreshed but still valid + } + } catch (err) { + console.warn("Failed to refresh token", err); + } + }; + return kc; + } catch (err) { + console.log(authenticated) + console.error("Keycloak init xyz failed", err); + initialized.value = true; + authenticated.value = false; + return kc; + } + }; + + const login = (options?: Keycloak.KeycloakLoginOptions) => { + if (!kc) throw new Error("Keycloak not initialized"); + return kc.login(options); + }; + + const logout = (redirectUri?: string) => { + if (!kc) throw new Error("Keycloak not initialized"); + return kc.logout({ redirectUri }); + }; + + const getToken = () => token.value; + const isAuthenticated = computed(() => authenticated.value); + const getProfile = () => profile.value; + + // init on client automatically + // onBeforeMount(() => { + // // try check-sso silently + // if (!initialized.value) initKeycloak("check-sso"); + // }); + + const apiErrors = ref>({}) + + const getResponse = async () => { + console.log("=================== onto login fes!!! ===================") + const params = { + token: token.value, + user: profile.value + } + const result = await xfetch('/api/v1/authentication/login-fes', 'POST', { + data: params, + }) + + if (result.success) { + const { data: rawdata, meta } = result.body + if (meta.status === 'verified') { + login(rawdata) + navigateTo('/') + } + } else { + if (result.errors) { + Object.entries(result.errors).forEach( + ([field, errorInfo]: [string, any]) => (apiErrors.value[field] = errorInfo.message), + ) + } else { + apiErrors.value.general = result.error?.message || result.message || 'Login failed' + } + } + } + + return { + initKeycloak, + login, + logout, + getToken, + isAuthenticated, + getProfile, + getResponse, + }; +} diff --git a/app/middleware/auth.global.ts b/app/middleware/auth.global.ts index 64d53a2f..82ec2703 100644 --- a/app/middleware/auth.global.ts +++ b/app/middleware/auth.global.ts @@ -1,14 +1,18 @@ -export default defineNuxtRouteMiddleware((to) => { +import { useKeycloak } from "~/composables/useKeycloack" + +export default defineNuxtRouteMiddleware(async (to) => { if (to.meta.public) return const { $pinia } = useNuxtApp() - const oidc = useOidcAuth(); + + const { initKeycloak, isAuthenticated} = useKeycloak(); // global composable + await initKeycloak("check-sso"); if (import.meta.client) { const userStore = useUserStore($pinia) - if (!userStore.isAuthenticated && !oidc.loggedIn) { + if (!userStore.isAuthenticated && !isAuthenticated.value) { + // await login({ redirectUri: window.location.origin + to.fullPath }); return navigateTo('/auth/login') - // oidc.login('dev'); } } }) diff --git a/app/pages/auth/sso.vue b/app/pages/auth/sso.vue index f2b21099..fbe6d959 100644 --- a/app/pages/auth/sso.vue +++ b/app/pages/auth/sso.vue @@ -1,13 +1,30 @@ diff --git a/nuxt.config.ts b/nuxt.config.ts index 06532995..7d5dd000 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -11,13 +11,11 @@ export default defineNuxtConfig({ X_AP_CODE: process.env.X_AP_CODE || 'rssa-sso', X_AP_SECRET_KEY: process.env.X_AP_SECRET_KEY || 'sapiperah', SSO_CONFIRM_URL: process.env.SSO_CONFIRM_URL || 'https://auth.rssa.top/realms/sandbox/protocol/openid-connect/userinfo', - NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID || 'portal-simrs-new', - NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET || 'awoiehrw3w8942341k1ln4', - NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL || 'https://auth.dev.rssa.id/realms/sandbox', KEYCLOAK_LOGOUT_REDIRECT: process.env.KEYCLOAK_LOGOUT_REDIRECT || 'http://localhost:3000', //test KEYCLOAK_REALM: process.env.KEYCLOAK_REALM || 'sandbox', KEYCLOAK_URL: process.env.KEYCLOAK_URL || 'https://auth.dev.rssa.id/', + CLIENT_ID: process.env.CLIENT_ID || 'portal-simrs-new', public: { API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000', VCLAIM: process.env.NUXT_API_VCLAIM || 'http://localhost:3000', @@ -26,13 +24,11 @@ export default defineNuxtConfig({ X_AP_CODE: process.env.X_AP_CODE || 'rssa-sso', X_AP_SECRET_KEY: process.env.X_AP_SECRET_KEY || 'sapiperah', SSO_CONFIRM_URL: process.env.SSO_CONFIRM_URL || 'https://auth.rssa.top/realms/sandbox/protocol/openid-connect/userinfo', - NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID || 'portal-simrs-new', - NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET || 'awoiehrw3w8942341k1ln4', - NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL || 'https://auth.dev.rssa.id/realms/sandbox', KEYCLOAK_LOGOUT_REDIRECT: process.env.KEYCLOAK_LOGOUT_REDIRECT || 'http://localhost:3000', //test KEYCLOAK_REALM: process.env.KEYCLOAK_REALM || 'sandbox', KEYCLOAK_URL: process.env.KEYCLOAK_URL || 'https://auth.dev.rssa.id/', + CLIENT_ID: process.env.CLIENT_ID || 'portal-simrs-new', }, }, ssr: false, @@ -68,24 +64,8 @@ export default defineNuxtConfig({ '@nuxtjs/color-mode', '@nuxtjs/tailwindcss', 'shadcn-nuxt', - 'nuxt-oidc-auth', ], - oidc: { - defaultProvider: 'keycloak', - keycloak: { - audience: 'account', - baseUrl: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_BASE_URL, // change to your OP addrress - clientId: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_ID, - clientSecret: process.env.NUXT_OIDC_PROVIDERS_KEYCLOAK_CLIENT_SECRET, - redirectUri: process.env.KEYCLOAK_LOGOUT_REDIRECT, // optional - }, - middleware: { - globalMiddlewareEnabled: true, - customLoginPage: true - } - }, - css: ['@unocss/reset/tailwind.css', '~/assets/css/main.css'], features: { diff --git a/package.json b/package.json index 95a51b70..98b42022 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "lucide-vue-next": "^0.482.0", "next-auth": "~4.21.1", "nuxt": "^4.0.3", - "nuxt-oidc-auth": "1.0.0-beta.5", "playwright-core": "^1.54.2", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.5.14", diff --git a/server/api/v1/authentication/login-fes.post.ts b/server/api/v1/authentication/login-fes.post.ts index abcd50e6..64d54a71 100644 --- a/server/api/v1/authentication/login-fes.post.ts +++ b/server/api/v1/authentication/login-fes.post.ts @@ -7,18 +7,8 @@ export default defineEventHandler(async (event) => { const url = getRequestURL(event) const config = useRuntimeConfig() - console.log("body: " + JSON.stringify(body)) - - // const apiSSOConfirm = 'https://auth.rssa.top/realms/sandbox/protocol/openid-connect/userinfo' const apiSSOConfirm = config.public.SSO_CONFIRM_URL - - const jwt = body.jwt - // const nip = body.nip - // const role = body.role - // const roleid = body.roleid - // const shift = body.shift - // const loginStatus = body.status_login - const token = 'Bearer ' + jwt + const token = 'Bearer ' + body.data.token const res_sso = await fetch(apiSSOConfirm, { @@ -31,40 +21,17 @@ export default defineEventHandler(async (event) => { console.log(res_sso) if (res_sso.status === 200) { - const parts = jwt.split('.') - - if (parts.count != 3) { - // return ['error' => 'Invalid JWT format']; - } - - const header = Buffer.from(strtr(parts[0], '-_', '+/'), 'base64').toString('utf8') - const payload = Buffer.from(strtr(parts[1], '-_', '+/'), 'base64').toString('utf8') - - // const textDecoder = new TextDecoder('utf-8'); - // // Decode the header and payload - // const decodedBinaryHead = atob(parts[0]); - // const decodedBinaryPayload = atob(parts[0]); - // const header = textDecoder.decode(Uint8Array.from(decodedBinaryHead, char => char.charCodeAt(0))); - // const payload = textDecoder.decode(Uint8Array.from(decodedBinaryPayload, char => char.charCodeAt(0))); - - const result = { - 'header': header, - 'payload': payload - }; - const apiOrigin = config.public.API_ORIGIN const cleanOrigin = apiOrigin.replace(/\/+$/, '') const cleanPath = url.pathname.replace(/^\/api\//, '').replace(/^\/+/, '') const externalUrl = `${cleanOrigin}/${cleanPath}${url.search}` - console.log("external url: " + externalUrl) - console.log("body: " + JSON.stringify(body)) const resp = await fetch(externalUrl, { method: 'POST', body: JSON.stringify({ - name: JSON.parse(payload).name, + name: body.data.user.username, }), headers: { 'Content-Type': 'application/json', @@ -73,27 +40,6 @@ export default defineEventHandler(async (event) => { }, }) - console.log(resp) - // if (resp.status === 200) { - // const data = await resp.json() - - // if (data?.data?.accessToken) { - // setCookie(event, 'authentication', data.data.accessToken, { - // path: '/', - // httpOnly: true, - // sameSite: 'strict', - // maxAge: 60 * 60 * 24, - // }) - - // delete data.data.accessToken - // // return data - - // const { login } = useUserStore() - // await login(resp.text()) - // await navigateTo('/') - // } - // } - return new Response(await resp.text(), { status: resp.status, headers: { @@ -109,14 +55,3 @@ export default defineEventHandler(async (event) => { }, }) }) - -function strtr(str: string, fromChars: string, toChars: string) { - let result = str; - for (let i = 0; i < fromChars.length; i++) { - const fromChar = fromChars[i] || '_-'; - // const toChar = toChars[i]; - // Use a global regex to replace all occurrences of the character - result = result.replace(new RegExp(fromChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), toChars); - } - return result; -}