From 806cfad6a8e9ee90bb2df5dd30a12be1ea940bb3 Mon Sep 17 00:00:00 2001 From: ari Date: Tue, 18 Nov 2025 11:31:57 +0700 Subject: [PATCH 01/12] update --- app/components/app/auth/login.vue | 9 ++ app/components/content/auth/login.vue | 69 +++++++++- nuxt.config.ts | 22 ++++ package.json | 3 + .../api/v1/authentication/login-fes.post.ts | 122 ++++++++++++++++++ 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 server/api/v1/authentication/login-fes.post.ts diff --git a/app/components/app/auth/login.vue b/app/components/app/auth/login.vue index 29fb60b4..170ab5d3 100644 --- a/app/components/app/auth/login.vue +++ b/app/components/app/auth/login.vue @@ -13,6 +13,7 @@ const props = defineProps() const emit = defineEmits<{ submit: [data: any] + sso: [] }>() const { handleSubmit, defineField, errors, meta } = useForm({ @@ -33,6 +34,10 @@ const onSubmit = handleSubmit(async (values) => { console.error('Submission failed:', error) } }) + +function onSSO() { + emit('sso') +} diff --git a/app/components/content/auth/login.vue b/app/components/content/auth/login.vue index 0e581ca9..5489fef5 100644 --- a/app/components/content/auth/login.vue +++ b/app/components/content/auth/login.vue @@ -1,5 +1,7 @@ diff --git a/nuxt.config.ts b/nuxt.config.ts index f4f0b231..588c79b8 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -7,10 +7,32 @@ export default defineNuxtConfig({ API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000', VCLAIM: process.env.NUXT_API_VCLAIM || 'http://localhost:3000', VCLAIM_SWAGGER: process.env.NUXT_API_VCLAIM_SWAGGER || 'http://localhost:3000', + //SSO + 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', + KEYCLOAK_ID: process.env.KEYCLOAK_ID || 'portal-simrs-new', + KEYCLOAK_SECRET: process.env.KEYCLOAK_SECRET || 'awoiehrw3w8942341k1ln4', + KEYCLOAK_ISSUER: process.env.KEYCLOAK_ISSUER || '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/', public: { API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000', VCLAIM: process.env.NUXT_API_VCLAIM || 'http://localhost:3000', VCLAIM_SWAGGER: process.env.NUXT_API_VCLAIM_SWAGGER || 'http://localhost:3000', + //SSO + 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', + KEYCLOAK_ID: process.env.KEYCLOAK_ID || 'portal-simrs-new', + KEYCLOAK_SECRET: process.env.KEYCLOAK_SECRET || 'awoiehrw3w8942341k1ln4', + KEYCLOAK_ISSUER: process.env.KEYCLOAK_ISSUER || '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/', }, }, ssr: false, diff --git a/package.json b/package.json index d415abde..d260052d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "lint": "eslint .", "format": "eslint --fix ." }, + "main": "./lib/keycloak.js", "dependencies": { "@iconify-json/lucide": "^1.2.30", "@iconify-json/radix-icons": "^1.2.2", @@ -24,6 +25,7 @@ "embla-carousel": "^8.5.2", "embla-carousel-vue": "^8.5.2", "h3": "^1.15.4", + "keycloak-js": "^26.2.1", "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.4.1", "tailwindcss-animate": "^1.0.7" @@ -50,6 +52,7 @@ "eslint-plugin-format": "^1.0.1", "happy-dom": "^18.0.1", "lucide-vue-next": "^0.482.0", + "next-auth": "~4.21.1", "nuxt": "^4.0.3", "playwright-core": "^1.54.2", "prettier": "^3.6.2", diff --git a/server/api/v1/authentication/login-fes.post.ts b/server/api/v1/authentication/login-fes.post.ts new file mode 100644 index 00000000..abcd50e6 --- /dev/null +++ b/server/api/v1/authentication/login-fes.post.ts @@ -0,0 +1,122 @@ +import { getRequestURL, readBody, setCookie } from 'h3' + +// Function to verify JWT token with the userinfo endpoint +export default defineEventHandler(async (event) => { + console.log("=================== MASUK FE SSO! ===================") + const body = await readBody(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 res_sso = await fetch(apiSSOConfirm, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': token, + } + }) + + 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, + }), + headers: { + 'Content-Type': 'application/json', + 'X-AuthPartner-Code': config.public.X_AP_CODE, + 'X-AuthPartner-SecretKey': config.public.X_AP_SECRET_KEY, + }, + }) + + 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: { + 'Content-Type': resp.headers.get('content-type') || 'text/plain', + }, + }) + } + + return new Response(await res_sso.text(), { + status: res_sso.status, + headers: { + 'Content-Type': res_sso.headers.get('content-type') || 'text/plain', + }, + }) +}) + +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; +} From d1fd8bb194628da7926c949ffb4a2c0717a7a115 Mon Sep 17 00:00:00 2001 From: ari Date: Tue, 18 Nov 2025 11:50:01 +0700 Subject: [PATCH 02/12] update format func --- app/components/app/auth/login.vue | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/components/app/auth/login.vue b/app/components/app/auth/login.vue index 170ab5d3..a10200eb 100644 --- a/app/components/app/auth/login.vue +++ b/app/components/app/auth/login.vue @@ -35,9 +35,16 @@ const onSubmit = handleSubmit(async (values) => { } }) -function onSSO() { - emit('sso') -} + +const onSSO = (async () => { + console.log("Emitting SSO...") + try { + await emit('sso') + } catch (error) { + console.error('Call SSO failed:', error) + } +}); +