diff --git a/api/src/routes/auth.js b/api/src/routes/auth.js index b9b985a..a0ec0b8 100644 --- a/api/src/routes/auth.js +++ b/api/src/routes/auth.js @@ -21,12 +21,12 @@ passport.use(new OpenIDConnectStrategy({ scope: ['openid', 'profile'] }, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { - console.log('--- OIDC verify() called ---'); - console.log('issuer:', issuer); - console.log('sub:', sub); - console.log('profile:', JSON.stringify(profile, null, 2)); - console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2)); - console.log('preferred_username:', jwtClaims?.preferred_username); + // console.log('--- OIDC verify() called ---'); + // console.log('issuer:', issuer); + // console.log('sub:', sub); + // console.log('profile:', JSON.stringify(profile, null, 2)); + // console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2)); + // console.log('preferred_username:', jwtClaims?.preferred_username); const con = await pool.getConnection(); try { @@ -34,14 +34,11 @@ passport.use(new OpenIDConnectStrategy({ //lookup existing user const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]); - console.log(existing) let memberId; //if member exists if (existing.length > 0) { - console.log('member exists'); memberId = existing[0].id; } else { - console.log("creating member") //otherwise: create account const username = sub.username; @@ -52,7 +49,6 @@ passport.use(new OpenIDConnectStrategy({ memberId = result.insertId; } - console.log("hello world" + memberId); await con.commit(); return cb(null, { memberId }); } catch (error) { @@ -63,11 +59,36 @@ passport.use(new OpenIDConnectStrategy({ } })); -router.get('/login', passport.authenticate('openidconnect')) -router.get('/callback', passport.authenticate('openidconnect', { - successRedirect: 'https://aj17thdev.nexuszone.net/', - failureRedirect: 'https://aj17thdev.nexuszone.net/' -})); +router.get('/login', (req, res, next) => { + // Store redirect target in session if provided + req.session.redirectTo = req.query.redirect || '/'; + + next(); +}, passport.authenticate('openidconnect')); + +// router.get('/callback', (req, res, next) => { +// passport.authenticate('openidconnect', { +// successRedirect: req.session.redirectTo, +// failureRedirect: 'https://aj17thdev.nexuszone.net/' +// }) +// }); + +router.get('/callback', (req, res, next) => { + const redirectURI = req.session.redirectTo; + passport.authenticate('openidconnect', (err, user) => { + if (err) return next(err); + if (!user) return res.redirect('https://aj17thdev.nexuszone.net/'); + + req.logIn(user, err => { + if (err) return next(err); + + // Use redirect saved from session + const redirectTo = redirectURI || 'https://aj17thdev.nexuszone.net/'; + delete req.session.redirectTo; + return res.redirect(redirectTo); + }); + })(req, res, next); +}); router.post('/logout', function (req, res, next) { req.logout(function (err) { @@ -75,14 +96,13 @@ router.post('/logout', function (req, res, next) { var params = { client_id: process.env.AUTH_CLIENT_ID, returnTo: 'https://aj17thdev.nexuszone.net/' - }; + }; res.redirect(process.env.AUTH_DOMAIN + '/v2/logout?' + querystring.stringify(params)); }); }); passport.serializeUser(function (user, cb) { process.nextTick(function () { - console.log(`serialize: ${user.memberId}`); cb(null, user); }); }); @@ -95,8 +115,7 @@ passport.deserializeUser(function (user, cb) { var userData; try { - userResults = await con.query(`SELECT id, name FROM members WHERE id = ?;`, [memberID]) - console.log(userResults) + let userResults = await con.query(`SELECT id, name FROM members WHERE id = ?;`, [memberID]) userData = userResults[0]; } catch (error) { diff --git a/api/src/routes/members.js b/api/src/routes/members.js index c9b4113..9424c65 100644 --- a/api/src/routes/members.js +++ b/api/src/routes/members.js @@ -2,11 +2,13 @@ const express = require('express'); const router = express.Router(); import pool from '../db'; +import { getUserRoles } from '../services/rolesService'; -//create a new user? -router.post('/', async (req, res) => { - -}); +router.use((req, res, next) => { + console.log(req.user); + console.log('Time:', Date.now()) + next() +}) //get all users router.get('/', async (req, res) => { @@ -42,12 +44,18 @@ router.get('/me', async (req, res) => { FROM leave_of_absences WHERE member_id = ? AND deleted = 0 - AND UTC_TIMESTAMP() BETWEEN start_date AND end_date;`, req.user.id) - const userWithLOA = { + AND UTC_TIMESTAMP() BETWEEN start_date AND end_date;`, req.user.id); + + const roleData = await getUserRoles(req.user.id); + + const userDataFull = { ...req.user, - loa: LOAData + loa: LOAData, + roles: roleData }; - res.json(userWithLOA); + + console.log(userDataFull); + res.status(200).json(userDataFull); } catch (error) { console.error('Error fetching LOA data:', error); return res.status(500).json({ error: 'Failed to fetch user data' }); diff --git a/api/src/routes/roles.js b/api/src/routes/roles.js index 666a754..f1857f6 100644 --- a/api/src/routes/roles.js +++ b/api/src/routes/roles.js @@ -3,14 +3,14 @@ const r = express.Router(); const ur = express.Router(); import pool from '../db'; +import { assignUserGroup, createGroup } from '../services/rolesService'; -//assign a member to a role +//manually assign a member to a group ur.post('/', async (req, res) => { try { const body = req.body; - const sql = `INSERT INTO members_roles (member_id, role_id) VALUES (?, ?);`; - await pool.query(sql, [body.member_id, body.role_id]); + assignUserGroup(body.member_id, body.role_id); res.sendStatus(201); } catch (err) { @@ -19,6 +19,7 @@ ur.post('/', async (req, res) => { } }); +//manually remove member from group ur.delete('/', async (req, res) => { try { const body = req.body; @@ -89,12 +90,9 @@ r.post('/', async (req, res) => { return res.status(400).json({ error: 'Color must be a valid hex color (#ffffff)' }); } - const sql = `INSERT INTO roles (name, color, description) VALUES (?, ?, ?);`; - const params = [name, color, description || null]; + await createGroup(name, color, description); - const result = await pool.query(sql, params); - - res.status(201).json({ id: result.insertId, name, color, description }); + res.sendStatus(201); } catch (err) { console.error('Insert failed:', err); res.status(500).json({ error: 'Failed to create role' }); diff --git a/api/src/services/calendarService.ts b/api/src/services/calendarService.ts index d110577..890889c 100644 --- a/api/src/services/calendarService.ts +++ b/api/src/services/calendarService.ts @@ -1,4 +1,3 @@ -// const pool = require('../db'); import pool from '../db'; export interface CalendarEvent { diff --git a/api/src/services/rolesService.ts b/api/src/services/rolesService.ts new file mode 100644 index 0000000..11fc3ff --- /dev/null +++ b/api/src/services/rolesService.ts @@ -0,0 +1,26 @@ +import pool from '../db'; + +export async function assignUserGroup(userID: number, roleID: number) { + + const sql = `INSERT INTO members_roles (member_id, role_id) VALUES (?, ?);`; + const params = [userID, roleID]; + + return await pool.query(sql, params); +} + +export async function createGroup(name: string, color: string, description: string) { + const sql = `INSERT INTO roles (name, color, description) VALUES (?, ?, ?)`; + const params = [name, color, description]; + + const result = await pool.query(sql, params); + return { id: result.insertId, name, color, description }; +} + +export async function getUserRoles(userID: number) { + const sql = `SELECT r.id, r.name + FROM members_roles mr + INNER JOIN roles r ON mr.role_id = r.id + WHERE mr.member_id = 190;`; + + return await pool.query(sql, [userID]); +} \ No newline at end of file diff --git a/ui/src/App.vue b/ui/src/App.vue index 41f7571..af9f817 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -16,13 +16,14 @@ import AlertDescription from './components/ui/alert/AlertDescription.vue'; const userStore = useUserStore(); -onMounted(async () => { - const res = await fetch(`${import.meta.env.VITE_APIHOST}/members/me`, { - credentials: 'include', - }); - const data = await res.json(); - userStore.user = data; -}); +// onMounted(async () => { +// const res = await fetch(`${import.meta.env.VITE_APIHOST}/members/me`, { +// credentials: 'include', +// }); +// const data = await res.json(); +// console.log(data); +// userStore.user = data; +// }); async function logout() { await fetch(`${import.meta.env.VITE_APIHOST}/logout`, { diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index e9a38c3..b97e7ac 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -12,7 +12,9 @@ export type Member = { const addr = import.meta.env.VITE_APIHOST; export async function getMembers(): Promise { - const response = await fetch(`${addr}/members`); + const response = await fetch(`${addr}/members`, { + credentials: 'include' + }); if (!response.ok) { throw new Error("Failed to fetch members"); } diff --git a/ui/src/pages/Application.vue b/ui/src/pages/Application.vue index 2c1d562..3db4b5d 100644 --- a/ui/src/pages/Application.vue +++ b/ui/src/pages/Application.vue @@ -23,6 +23,7 @@ onMounted(async () => { const router = useRoute(); const appIDRaw = router.params.id.toString(); const raw = await loadApplication(appIDRaw); + console.log(raw); if (raw === null) { //new app appData.value = null @@ -43,7 +44,7 @@ onMounted(async () => { readOnly.value = true; } } catch (e) { - console.error(e) + console.error(e); } loading.value = false; }) diff --git a/ui/src/pages/Join.vue b/ui/src/pages/Join.vue new file mode 100644 index 0000000..f3fa0fe --- /dev/null +++ b/ui/src/pages/Join.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/ui/src/pages/Unauthorized.vue b/ui/src/pages/Unauthorized.vue new file mode 100644 index 0000000..81340d7 --- /dev/null +++ b/ui/src/pages/Unauthorized.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/ui/src/router/index.js b/ui/src/router/index.js index ec22e5d..2541e6a 100644 --- a/ui/src/router/index.js +++ b/ui/src/router/index.js @@ -1,45 +1,67 @@ +import { useUserStore } from '@/stores/user' import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ - { path: '/applications', component: () => import('@/pages/ManageApplications.vue') }, - { path: '/applications/:id', component: () => import('@/pages/Application.vue') }, - { path: '/rankChange', component: () => import('@/pages/RankChange.vue') }, - { path: '/members', component: () => import('@/pages/memberList.vue') }, - { path: '/loa', component: () => import('@/pages/SubmitLOA.vue') }, - { path: '/transfer', component: () => import('@/pages/Transfer.vue') }, - { path: '/calendar', component: () => import('@/pages/Calendar.vue') }, + // PUBLIC + { path: '/join', component: () => import('@/pages/Join.vue') }, + + // AUTH REQUIRED + { path: '/apply', component: () => import('@/pages/Application.vue'), meta: { requiresAuth: true } }, + + // MEMBER ROUTES + { path: '/members', component: () => import('@/pages/memberList.vue'), meta: { requiresAuth: true, memberOnly: true } }, + { path: '/loa', component: () => import('@/pages/SubmitLOA.vue'), meta: { requiresAuth: true, memberOnly: true } }, + { path: '/transfer', component: () => import('@/pages/Transfer.vue'), meta: { requiresAuth: true, memberOnly: true } }, + { path: '/calendar', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true } }, + + // ADMIN / STAFF ROUTES { path: '/administration', + meta: { requiresAuth: true, memberOnly: true, roles: ['staff', 'admin'] }, children: [ - { - path: 'applications', - component: () => import('@/pages/ManageApplications.vue') - }, - { - path: 'rankChange', - component: () => import('@/pages/RankChange.vue') - }, - { - path: 'applications/:id', - component: () => import('@/pages/Application.vue') - }, - { - path: 'transfer', - component: () => import('@/pages/ManageTransfers.vue') - }, - { - path: 'loa', - component: () => import('@/pages/ManageLOA.vue') - }, - { - path: 'roles', - component: () => import('@/pages/ManageRoles.vue') - } + { path: 'applications', component: () => import('@/pages/ManageApplications.vue') }, + { path: 'rankChange', component: () => import('@/pages/RankChange.vue') }, + { path: 'applications/:id', component: () => import('@/pages/Application.vue') }, + { path: 'transfer', component: () => import('@/pages/ManageTransfers.vue') }, + { path: 'loa', component: () => import('@/pages/ManageLOA.vue') }, + { path: 'roles', component: () => import('@/pages/ManageRoles.vue') } ] - } + }, + + // UNAUTHORIZED PAGE + { path: '/unauthorized', component: () => import('@/pages/Unauthorized.vue') } ] }) -export default router +router.beforeEach(async (to) => { + const userStore = useUserStore() + + // Make sure user state is loaded before checking + if (!userStore.loaded) { + console.log('loaduser') + await userStore.loadUser(); + } + + // Not logged in + if (to.meta.requiresAuth && !userStore.isLoggedIn) { + // Redirect back to original page after login + const redirectUrl = encodeURIComponent(window.location.origin + to.fullPath) + window.location.href = `https://aj17thdevapi.nexuszone.net/login?redirect=${redirectUrl}` + return false // Prevent Vue Router from continuing + } + + + // // Must be a member + // if (to.meta.memberOnly && userStore.status !== 'member') { + // return '/unauthorized' + // } + + // // Must have specific role + // if (to.meta.roles && !to.meta.roles.includes(userStore.role)) { + // return '/unauthorized' + // } +}) + +export default router; \ No newline at end of file diff --git a/ui/src/stores/user.ts b/ui/src/stores/user.ts index 6e779c2..2e9ca64 100644 --- a/ui/src/stores/user.ts +++ b/ui/src/stores/user.ts @@ -3,8 +3,25 @@ import { defineStore } from 'pinia' export const useUserStore = defineStore('user', () => { const user = ref(null) - const roles = ref([]) + const roles = computed(() => { user.value.roles }) + const loaded = ref(false); const isLoggedIn = computed(() => user.value !== null) - return { user, isLoggedIn, roles } + + async function loadUser() { + //@ts-ignore + const res = await fetch(`${import.meta.env.VITE_APIHOST}/members/me`, { + credentials: 'include', + }); + + if (res.ok) { + const data = await res.json(); + console.log(data); + user.value = data; + } + + loaded.value = true; + } + + return { user, isLoggedIn, roles, loadUser, loaded } })