feat: link sfids
This commit is contained in:
parent
c89ff7e61e
commit
5069965ba3
11 changed files with 371 additions and 11 deletions
|
|
@ -45,11 +45,12 @@ export const useAuth = () => {
|
|||
window.history.replaceState({}, document.title, window.location.pathname)
|
||||
await verifySession()
|
||||
startLiveSessionPolling()
|
||||
return true
|
||||
|
||||
return { success: true, isFirstLogin: data.isFirstLogin }
|
||||
} catch (error) {
|
||||
console.error('Code exchange failed:', error)
|
||||
clearSession()
|
||||
return false
|
||||
return { success: false, isFirstLogin: false }
|
||||
} finally {
|
||||
isInitialized.value = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ onMounted(async () => {
|
|||
|
||||
if (route.query.code) {
|
||||
isLoggingIn.value = true
|
||||
const success = await exchangeCodeForToken(route.query.code)
|
||||
const result = await exchangeCodeForToken(route.query.code)
|
||||
|
||||
if (success) {
|
||||
await navigateTo('/')
|
||||
if (result?.success) {
|
||||
if (result.isFirstLogin) {
|
||||
await navigateTo('/welcome')
|
||||
} else {
|
||||
await navigateTo('/')
|
||||
}
|
||||
} else {
|
||||
errorMessage.value = 'Access Denied'
|
||||
isLoggingIn.value = false
|
||||
|
|
@ -31,7 +35,6 @@ const handleLogin = () => {
|
|||
errorMessage.value = ''
|
||||
|
||||
const redirectUri = `${window.location.origin}/oauth`
|
||||
|
||||
const authUrl = new URL(
|
||||
`${config.public.keycloakUrl}/realms/${config.public.keycloakRealm}/protocol/openid-connect/auth`
|
||||
)
|
||||
|
|
|
|||
145
app/pages/welcome/index.vue
Normal file
145
app/pages/welcome/index.vue
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { userToken } = useAuth()
|
||||
|
||||
const sfidInput = ref('')
|
||||
const step = ref(1)
|
||||
const foundUser = ref(null)
|
||||
|
||||
const isSearching = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const handleLookup = async () => {
|
||||
if (!sfidInput.value.trim()) return
|
||||
|
||||
isSearching.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const res = await $fetch('/api/user/lookup-nnas', {
|
||||
method: 'POST',
|
||||
body: { sfid: sfidInput.value }
|
||||
})
|
||||
|
||||
foundUser.value = res
|
||||
step.value = 2
|
||||
} catch (err) {
|
||||
errorMessage.value = err.data?.statusMessage || err.data?.message || 'Failed to locate user account.'
|
||||
} finally {
|
||||
isSearching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!foundUser.value) return
|
||||
|
||||
isSubmitting.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await $fetch('/api/user/complete-welcome', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
token: userToken.value,
|
||||
sfid: foundUser.value.username
|
||||
}
|
||||
})
|
||||
await navigateTo('/')
|
||||
} catch (err) {
|
||||
errorMessage.value = err.data?.statusMessage || err.data?.message || 'Failed to complete setup.'
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
step.value = 1
|
||||
foundUser.value = null
|
||||
errorMessage.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<section class="settings-panel" style="max-width: 480px; margin: 0 auto;">
|
||||
<div class="panel-header archivo-black">
|
||||
WELCOME
|
||||
</div>
|
||||
|
||||
<div v-if="step === 1" class="panel-body" style="padding: 2rem; text-align: center;">
|
||||
<img src="/welcome5-1.png" alt="Welcome Image"
|
||||
style="max-width: 160px; margin-bottom: 1rem; object-fit: contain;" />
|
||||
|
||||
<h2 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem;">
|
||||
First Time Setup
|
||||
</h2>
|
||||
|
||||
<p style="color: #8b949e; font-size: 0.9rem; margin-bottom: 1.5rem; line-height: 1.4;">
|
||||
Please enter your SFID to link your account. This wont be used for login.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="handleLookup">
|
||||
<div style="margin-bottom: 1.25rem; text-align: left;">
|
||||
<label for="sfid" style="display: block; font-size: 0.85rem; color: #c9d1d9; margin-bottom: 0.5rem;">
|
||||
SFID <span style="color: #f85149;">*</span>
|
||||
</label>
|
||||
<input id="sfid" v-model="sfidInput" type="text" placeholder="e.g. redbinderischud" required
|
||||
style="width: 100%; padding: 0.6rem 0.8rem; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-size: 0.95rem; outline: none;" />
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" style="color: #f85149; font-size: 0.85rem; margin-bottom: 1rem;">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<button type="submit" class="btn-login-action" style="width: 100%;" :disabled="isSearching">
|
||||
<span v-if="isSearching" class="spinner sm btn-spinner"></span>
|
||||
<span style="vertical-align: middle;">
|
||||
{{ isSearching ? 'Looking up...' : 'Find Account →' }}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-else class="panel-body" style="padding: 2rem; text-align: center;">
|
||||
<h2 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 1.5rem;">
|
||||
Is this you?
|
||||
</h2>
|
||||
|
||||
<div
|
||||
style="background: #0d1117; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<img :src="`https://mii.spfn.net/${foundUser.pid}/main.png`" :alt="foundUser.username"
|
||||
style="width: 96px; height: 96px; border-radius: 50%; background: #161b22; object-fit: contain; margin-bottom: 0.75rem;"
|
||||
@error="(e) => e.target.src = '/img_unknown_MiiIcon.png'" />
|
||||
<div style="font-size: 1.1rem; font-weight: 600; color: #f0f6fc;">
|
||||
{{ foundUser.username }}
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: #8b949e; margin-top: 0.25rem;">
|
||||
PID: {{ foundUser.pid }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" style="color: #f85149; font-size: 0.85rem; margin-bottom: 1rem;">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button type="button"
|
||||
style="flex: 1; padding: 0.6rem; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-weight: 500; cursor: pointer;"
|
||||
:disabled="isSubmitting" @click="handleReset">
|
||||
Not Me
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn-login-action" style="flex: 1;" :disabled="isSubmitting"
|
||||
@click="handleConfirm">
|
||||
<span v-if="isSubmitting" class="spinner sm btn-spinner"></span>
|
||||
<span style="vertical-align: middle;">
|
||||
{{ isSubmitting ? 'Linking...' : 'Yes, That\'s Me!' }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
29
bun.lock
29
bun.lock
|
|
@ -8,6 +8,7 @@
|
|||
"@nuxt/ui": "^4.10.0",
|
||||
"keycloak-js": "^26.2.4",
|
||||
"nuxt": "^4.5.0",
|
||||
"pg": "^8.22.0",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vue": "^3.5.40",
|
||||
"vue-router": "^5.2.0",
|
||||
|
|
@ -1148,6 +1149,22 @@
|
|||
|
||||
"perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
|
||||
|
||||
"pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.14.0", "", {}, "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.15.0", "", {}, "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
|
@ -1214,6 +1231,14 @@
|
|||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"pretty-bytes": ["pretty-bytes@7.1.1", "", {}, "sha512-X+vn9z8nOFZQlxOLmfJ0iKDdMD7jYTsTW12OAlCpdoE3Igik6L37pugIZi+N3usuyp5McfgKPWi12q3zvHLeGQ=="],
|
||||
|
|
@ -1342,6 +1367,8 @@
|
|||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
|
@ -1520,6 +1547,8 @@
|
|||
|
||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"y-protocols": ["y-protocols@1.0.7", "", { "dependencies": { "lib0": "^0.2.85" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"@nuxt/ui": "^4.10.0",
|
||||
"keycloak-js": "^26.2.4",
|
||||
"nuxt": "^4.5.0",
|
||||
"pg": "^8.22.0",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vue": "^3.5.40",
|
||||
"vue-router": "^5.2.0"
|
||||
|
|
|
|||
BIN
public/img_unknown_MiiIcon.png
Normal file
BIN
public/img_unknown_MiiIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -1,3 +1,5 @@
|
|||
import { query } from '../../utils/db'
|
||||
|
||||
function parseJwt(token: string) {
|
||||
try {
|
||||
const base64Payload = token.split('.')[1]
|
||||
|
|
@ -35,9 +37,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
const response = await $fetch<any>(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: params.toString()
|
||||
})
|
||||
|
||||
|
|
@ -51,11 +51,37 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
return response
|
||||
const keycloakId = decodedToken?.sub
|
||||
const email = decodedToken?.email
|
||||
|
||||
let isFirstLogin = false
|
||||
|
||||
if (keycloakId) {
|
||||
try {
|
||||
const dbResult = await query(
|
||||
`INSERT INTO users (keycloak_id, email)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (keycloak_id) DO UPDATE SET updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING first_login_completed`,
|
||||
[keycloakId, email]
|
||||
)
|
||||
|
||||
isFirstLogin = !dbResult.rows[0]?.first_login_completed
|
||||
} catch (dbErr) {
|
||||
console.error('[DB ERROR in token.post.ts]:', dbErr)
|
||||
isFirstLogin = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
isFirstLogin
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[AUTH ERROR in token.post.ts]:', error?.data || error)
|
||||
throw createError({
|
||||
statusCode: error.statusCode || error.response?.status || 401,
|
||||
statusMessage: error.data?.message || error.response?._data?.error_description || 'Failed to exchange code'
|
||||
statusMessage: error.data?.message || error.response?._data?.error_description || 'Authentication failed'
|
||||
})
|
||||
}
|
||||
})
|
||||
69
server/api/user/complete-welcome.post.ts
Normal file
69
server/api/user/complete-welcome.post.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { query } from '../../utils/db'
|
||||
import { queryNnas } from '../../utils/nnasDb'
|
||||
|
||||
function parseJwt(token: string) {
|
||||
try {
|
||||
const base64Payload = token.split('.')[1]
|
||||
return JSON.parse(Buffer.from(base64Payload, 'base64').toString('utf8'))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const decoded = body?.token ? parseJwt(body.token) : null
|
||||
|
||||
if (!decoded?.sub) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
||||
}
|
||||
|
||||
const usernameInput = body?.sfid?.trim()
|
||||
|
||||
if (!usernameInput) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'SFID is required' })
|
||||
}
|
||||
|
||||
let nnasUser: { pid: number; username: string } | null = null
|
||||
|
||||
try {
|
||||
const nnasResult = await queryNnas(
|
||||
`SELECT pid, username FROM users WHERE LOWER(username) = LOWER($1) LIMIT 1`,
|
||||
[usernameInput]
|
||||
)
|
||||
|
||||
if (nnasResult.rows.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `SFID "${usernameInput}" was not found in NNAS database.`
|
||||
})
|
||||
}
|
||||
|
||||
nnasUser = nnasResult.rows[0]
|
||||
} catch (error: any) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('[NNAS DB Error]:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to query NNAS database.'
|
||||
})
|
||||
}
|
||||
|
||||
await query(
|
||||
`UPDATE users
|
||||
SET sfid = $1,
|
||||
pid = $2,
|
||||
first_login_completed = TRUE,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE keycloak_id = $3`,
|
||||
[nnasUser.username, nnasUser.pid, decoded.sub]
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: {
|
||||
sfid: nnasUser.username,
|
||||
pid: nnasUser.pid
|
||||
}
|
||||
}
|
||||
})
|
||||
36
server/api/user/lookup-nnas.post.ts
Normal file
36
server/api/user/lookup-nnas.post.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { queryNnas } from '../../utils/nnasDb'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const usernameInput = body?.sfid?.trim()
|
||||
|
||||
if (!usernameInput) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Username is required' })
|
||||
}
|
||||
|
||||
try {
|
||||
const nnasResult = await queryNnas(
|
||||
`SELECT pid, username FROM users WHERE LOWER(username) = LOWER($1) LIMIT 1`,
|
||||
[usernameInput]
|
||||
)
|
||||
|
||||
if (nnasResult.rows.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `Account "${usernameInput}" was not found.`
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
pid: nnasResult.rows[0].pid,
|
||||
username: nnasResult.rows[0].username
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('[NNAS Lookup Error]:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to look up NNAS profile'
|
||||
})
|
||||
}
|
||||
})
|
||||
39
server/utils/db.ts
Normal file
39
server/utils/db.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import pg from 'pg'
|
||||
|
||||
const pool = new pg.Pool({
|
||||
host: process.env.POSTGRES_HOST || 'localhost',
|
||||
port: Number(process.env.POSTGRES_PORT) || 5432,
|
||||
database: process.env.POSTGRES_DB || 'mod_portal',
|
||||
user: process.env.POSTGRES_USER || 'postgres',
|
||||
password: process.env.POSTGRES_PASSWORD || 'postgres',
|
||||
})
|
||||
|
||||
const initDb = async () => {
|
||||
try {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
keycloak_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
email VARCHAR(255),
|
||||
first_login_completed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
sfid VARCHAR(255),
|
||||
pid INTEGER
|
||||
);
|
||||
`)
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS sfid VARCHAR(255);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS pid INTEGER;
|
||||
`)
|
||||
|
||||
console.log('[DB INIT]: Table "users" checked/updated successfully.')
|
||||
} catch (err) {
|
||||
console.error('[DB INIT ERROR]:', err)
|
||||
}
|
||||
}
|
||||
|
||||
initDb()
|
||||
|
||||
export const query = (text: string, params?: any[]) => pool.query(text, params)
|
||||
11
server/utils/nnasDb.ts
Normal file
11
server/utils/nnasDb.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import pg from 'pg'
|
||||
|
||||
const nnasPool = new pg.Pool({
|
||||
host: process.env.NNAS_POSTGRES_HOST || 'localhost',
|
||||
port: Number(process.env.NNAS_POSTGRES_PORT) || 5432,
|
||||
database: process.env.NNAS_POSTGRES_DB || 'NNAS',
|
||||
user: process.env.NNAS_POSTGRES_USER || 'postgres',
|
||||
password: process.env.NNAS_POSTGRES_PASSWORD || 'postgres',
|
||||
})
|
||||
|
||||
export const queryNnas = (text: string, params?: any[]) => nnasPool.query(text, params)
|
||||
Loading…
Reference in a new issue