Did more stuff than I even wanna write. Notably:

- Auth/account management
- Navigation system
- Admin views for LOA stuff
This commit is contained in:
2025-09-18 20:33:19 -04:00
parent 4fcd485e75
commit f708349a99
20 changed files with 2139 additions and 85 deletions

View File

@@ -13,7 +13,28 @@ app.use(cors({
app.use(express.json())
const port = 3000;
app.set('trust proxy', 1);
const port = process.env.SERVER_PORT;
//session setup
const path = require('path')
const session = require('express-session')
const passport = require('passport')
const SQLiteStore = require('connect-sqlite3')(session);
app.use(session({
secret: 'whatever',
resave: false,
saveUninitialized: false,
store: new SQLiteStore({ db: 'sessions.db', dir: './' }),
cookie: {
httpOnly: true,
sameSite: 'lax',
domain: 'nexuszone.net'
}
}));
app.use(passport.authenticate('session'));
// Mount route modules
const applicationsRouter = require('./routes/applications');
@@ -21,6 +42,7 @@ const { memberRanks, ranks } = require('./routes/ranks');
const members = require('./routes/members');
const loaHandler = require('./routes/loa')
const { status, memberStatus } = require('./routes/statuses')
const authRouter = require('./routes/auth')
app.use('/application', applicationsRouter);
app.use('/ranks', ranks);
@@ -29,6 +51,8 @@ app.use('/members', members);
app.use('/loa', loaHandler);
app.use('/status', status)
app.use('/memberStatus', memberStatus)
app.use('/', authRouter)
app.get('/ping', (req, res) => {
res.status(200).json({ message: 'pong' });
});

1515
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,14 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"connect-sqlite3": "^0.9.16",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-session": "^1.18.2",
"mariadb": "^3.4.5",
"mysql2": "^3.14.3"
"mysql2": "^3.14.3",
"passport": "^0.7.0",
"passport-openidconnect": "^0.1.2"
}
}

110
api/routes/auth.js Normal file
View File

@@ -0,0 +1,110 @@
const passport = require('passport');
const OpenIDConnectStrategy = require('passport-openidconnect');
const dotenv = require('dotenv');
dotenv.config();
const express = require('express');
const { param } = require('./applications');
const router = express.Router();
const pool = require('../db')
passport.use(new OpenIDConnectStrategy({
issuer: process.env.AUTH_ISSUER,
authorizationURL: 'https://sso.iceberg-gaming.com/application/o/authorize/',
tokenURL: 'https://sso.iceberg-gaming.com/application/o/token/',
userInfoURL: 'https://sso.iceberg-gaming.com/application/o/userinfo/',
clientID: process.env.AUTH_CLIENT_ID,
clientSecret: process.env.AUTH_CLIENT_SECRET,
callbackURL: process.env.AUTH_REDIRECT_URI,
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);
const con = await pool.getConnection();
try {
await con.beginTransaction();
//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;
const result = await con.query(
`INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`,
[username, sub, issuer]
)
memberId = result.insertId;
}
console.log("hello world" + memberId);
await con.commit();
return cb(null, { memberId });
} catch (error) {
await con.rollback();
return cb(error);
} finally {
con.release();
}
}));
router.get('/login', passport.authenticate('openidconnect'))
router.get('/callback', passport.authenticate('openidconnect', {
successRedirect: 'https://aj17thdev.nexuszone.net/',
failureRedirect: 'https://aj17thdev.nexuszone.net/'
}));
router.post('/logout', function (req, res, next) {
req.logout(function (err) {
if (err) { return next(err); }
var params = {
client_id: process.env.AUTH_CLIENT_ID,
returnTo: 'https://aj17thdev.nexuszone.net/'
};
res.redirect(process.env.AUTH_DOMAIN + '/v2/logout?' + qs.stringify(params));
});
});
passport.serializeUser(function (user, cb) {
process.nextTick(function () {
console.log(`serialize: ${user.memberId}`);
cb(null, user);
});
});
passport.deserializeUser(function (user, cb) {
process.nextTick(async function () {
const memberID = user.memberId;
const con = await pool.getConnection();
var userData;
try {
userResults = await con.query(`SELECT id, name FROM members WHERE id = ?;`, [memberID])
console.log(userResults)
userData = userResults[0];
} catch (error) {
console.error(error)
} finally {
con.release();
}
return cb(null, userData);
});
});
module.exports = router;

View File

@@ -40,4 +40,18 @@ router.get("/me", async (req, res) => {
}
})
router.get('/all', async (req, res) => {
try {
const result = await pool.query(
`SELECT loa.*, members.name
FROM leave_of_absences AS loa
INNER JOIN members ON loa.member_id = members.id;
`);
res.status(200).json(result)
} catch (error) {
console.error(error);
res.status(500).send(error);
}
})
module.exports = router;

46
api/routes/statuses.js Normal file
View File

@@ -0,0 +1,46 @@
const express = require('express');
const status = express.Router();
const memberStatus = express.Router();
const pool = require('../db');
//insert a new latest rank for a user
memberStatus.post('/', async (req, res) => {
// try {
// const App = req.body?.App || {};
// // TODO: replace with current user ID
// const memberId = 1;
// const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`;
// const appVersion = 1;
// const params = [memberId, appVersion, JSON.stringify(App)]
// console.log(params)
// await pool.query(sql, params);
// res.sendStatus(201);
// } catch (err) {
// console.error('Insert failed:', err);
// res.status(500).json({ error: 'Failed to save application' });
// }
res.status(501).json({ error: 'Not implemented' });
});
//get all statuses
status.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM statuses;');
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports.status = status;
module.exports.memberStatus = memberStatus;
// TODO, implement get all ranks route with SQL stirng SELECT id, name, short_name, category, sort_id FROM ranks;

View File

@@ -1,44 +0,0 @@
const express = require('express');
const router = express.Router();
// DB pool (same as used in api/index.js)
const pool = require('../db');
//create a new user?
router.post('/', async (req, res) => {
});
//get all users
router.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM view_member_rank_status_all;');
return res.status(200).json(result);
} catch (err) {
console.error('Error fetching users:', err);
return res.status(500).json({ error: 'Failed to fetch users' });
}
});
router.get('/:id', async (req, res) => {
try {
const userId = req.params.id;
const result = await pool.query('SELECT * FROM view_member_rank_status_all WHERE id = $1;', [userId]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
return res.status(200).json(result.rows[0]);
} catch (err) {
console.error('Error fetching user:', err);
return res.status(500).json({ error: 'Failed to fetch user' });
}
});
//update a user's display name (stub)
router.put('/:id/displayname', async (req, res) => {
// Stub: not implemented yet
return res.status(501).json({ error: 'Update display name not implemented' });
});
module.exports = router;

View File

@@ -2,24 +2,99 @@
import { RouterLink, RouterView } from 'vue-router';
import Separator from './components/ui/separator/Separator.vue';
import Button from './components/ui/button/Button.vue';
import Application from './pages/Application.vue';
import AutoForm from './components/form/AutoForm.vue';
import ManageApplications from './pages/ManageApplications.vue';
import { Popover, PopoverContent, PopoverTrigger } from './components/ui/popover';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from './components/ui/dropdown-menu';
import { onMounted } from 'vue';
import { useUserStore } from './stores/user';
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;
});
</script>
<template>
<div>
<div class="h-15 flex items-center justify-center gap-20">
<Button variant="link">Link</Button>
<Button variant="link">Link</Button>
<Button variant="link">Link</Button>
<Button variant="link">Link</Button>
<div class="flex items-center justify-between px-10">
<div></div>
<div class="h-15 flex items-center justify-center gap-20">
<RouterLink to="/">
<Button variant="link">Home</Button>
</RouterLink>
<!-- <RouterLink to="/">
<Button variant="link">Calendar</Button>
</RouterLink> -->
<RouterLink to="/members">
<Button variant="link">Members</Button>
</RouterLink>
<Popover>
<PopoverTrigger as-child>
<Button variant="link">Forms</Button>
</PopoverTrigger>
<PopoverContent class="flex flex-col gap-4 items-center w-min">
<RouterLink to="/transfer">
<Button variant="link">Transfer Request</Button>
</RouterLink>
<RouterLink to="/trainingReport">
<Button variant="link">Training Report</Button>
</RouterLink>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger as-child>
<Button variant="link">Administration</Button>
</PopoverTrigger>
<PopoverContent class="flex flex-col gap-4 items-center w-min">
<RouterLink to="/administration/rankChange">
<Button variant="link">Promotions</Button>
</RouterLink>
<RouterLink to="/administration/loa">
<Button variant="link">Leave of Absence</Button>
</RouterLink>
<RouterLink to="/administration/transfer">
<Button variant="link">Transfer Requests</Button>
</RouterLink>
<RouterLink to="/administration/applications">
<Button variant="link">Recruitment</Button>
</RouterLink>
</PopoverContent>
</Popover>
</div>
<div>
<DropdownMenu v-if="userStore.isLoggedIn">
<DropdownMenuTrigger class="cursor-pointer">
<p>Profile</p>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>My Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>
<RouterLink to="/loa">
Submit LOA
</RouterLink>
</DropdownMenuItem>
<DropdownMenuItem :variant="'destructive'">Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<a v-else href="https://aj17thdevapi.nexuszone.net/login">Login</a>
</div>
</div>
<Separator></Separator>
<!-- <Application></Application> -->
<!-- <ManageApplications></ManageApplications> -->
<!-- <AutoForm class="max-w-3xl mx-auto my-20"></AutoForm> -->
<RouterView></RouterView>
<RouterView></RouterView>
</div>
</template>

View File

@@ -1,4 +1,6 @@
export type LOARequest = {
id: number;
name?: string;
member_id: number;
filed_date: string; // ISO 8601 string
start_date: string; // ISO 8601 string
@@ -43,3 +45,18 @@ export async function getMyLOA(): Promise<LOARequest | null> {
return null;
}
}
export function getAllLOAs(): Promise<LOARequest[]> {
return fetch(`${addr}/loa/all`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
}).then((res) => {
if (res.ok) {
return res.json();
} else {
return [];
}
});
}

34
ui/src/api/status.ts Normal file
View File

@@ -0,0 +1,34 @@
export type Status = {
id: number;
name: string;
created_at: string; // datetime as ISO string
updated_at: string; // datetime as ISO string
deleted?: boolean; // tinyint, optional if nullable
};
// @ts-ignore
const addr = import.meta.env.VITE_APIHOST;
export async function getAllStatuses(): Promise<Status[]> {
const res = await fetch(`${addr}/status`)
if (res.ok) {
return res.json()
} else {
console.error("Something went wrong getting statuses")
}
}
export async function assignStatus(userId: number, statusId: number, rankId: number): Promise<void> {
const res = await fetch(`${addr}/memberStatus`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ userId, statusId, rankId })
})
if (!res.ok) {
console.error("Something went wrong assigning the status")
}
}

View File

@@ -91,12 +91,12 @@ async function handleSubmit() {
}
function toMariaDBDatetime(date: Date): string {
return date.toISOString().slice(0, 19).replace('T', ' ');
return date.toISOString().slice(0, 19).replace('T', ' ');
}
</script>
<template>
<div class="flex flex-row-reverse gap-6 mx-auto m-10" :class="!adminMode ? 'max-w-5xl' : 'max-w-2xl'">
<div class="flex flex-row-reverse gap-6 mx-auto " :class="!adminMode ? 'max-w-5xl' : 'max-w-2xl'">
<div v-if="!adminMode" class="flex-1 flex space-x-4 rounded-md border p-4">
<div class="flex-2 space-y-1">
<p class="text-sm font-medium leading-none">
@@ -156,11 +156,7 @@ function toMariaDBDatetime(date: Date): string {
</PopoverContent>
</Popover>
</div>
<Textarea
v-model="reason"
placeholder="Reason for LOA"
class="w-full resize-none"
/>
<Textarea v-model="reason" placeholder="Reason for LOA" class="w-full resize-none" />
<div class="flex justify-end">
<Button :onClick="handleSubmit" :disabled="submitting" class="w-min">Submit</Button>
</div>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { getAllLOAs, LOARequest } from "@/api/loa";
import { onMounted, ref } from "vue";
const LOAList = ref<LOARequest[]>([]);
onMounted(async () => {
LOAList.value = await getAllLOAs();
});
function formatDate(dateStr: string): string {
if (!dateStr) return "";
return new Date(dateStr).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
}
</script>
<template>
<div class="w-5xl mx-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Member</TableHead>
<TableHead>Start</TableHead>
<TableHead>End</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Posted on</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="post in LOAList"
:key="post.id"
class="hover:bg-muted/50"
>
<TableCell class="font-medium">
{{ post.name }}
</TableCell>
<TableCell>{{ formatDate(post.start_date) }}</TableCell>
<TableCell>{{ formatDate(post.end_date) }}</TableCell>
<TableCell>{{ post.reason }}</TableCell>
<TableCell>{{ formatDate(post.filed_date) }}</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { Primitive } from "reka-ui";
import { cn } from "@/lib/utils";
import { badgeVariants } from ".";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
variant: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
</script>
<template>
<Primitive
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,24 @@
import { cva } from "class-variance-authority";
export { default as Badge } from "./Badge.vue";
export const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import LoaForm from '@/components/loa/loaForm.vue';
import LoaList from '@/components/loa/loaList.vue';
</script>
<template>
<div class="max-w-5xl mx-auto pt-10">
<!-- <LoaForm class="m-10"></LoaForm> -->
<h1>LOA Log</h1>
<LoaList></LoaList>
</div>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<LoaForm class="m-10"></LoaForm>
</template>

95
ui/src/pages/Transfer.vue Normal file
View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
import { Check, Search } from "lucide-vue-next"
import { Combobox, ComboboxAnchor, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxItemIndicator, ComboboxList } from "@/components/ui/combobox"
import { onMounted, ref } from "vue";
import { Member, getMembers } from "@/api/member";
import Button from "@/components/ui/button/Button.vue";
import { Status, getAllStatuses, assignStatus } from "@/api/status";
import { Rank, getRanks } from "@/api/rank";
const members = ref<Member[]>([])
const statuses = ref<Status[]>([])
const allRanks = ref<Rank[]>([])
const currentMember = ref<Member | null>(null);
const currentStatus = ref<Status | null>(null);
const currentRank = ref<Rank | null>(null);
onMounted(async () => {
members.value = await getMembers();
statuses.value = await getAllStatuses();
allRanks.value = await getRanks();
});
</script>
<template>
<div class="flex flex-row gap-6 mx-auto m-10 max-w-5xl">
<Combobox v-model="currentMember">
<ComboboxAnchor class="w-[300px]">
<ComboboxInput placeholder="Search members..." class="w-full pl-9"
:display-value="(v) => v ? v.member_name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-[300px]">
<ComboboxEmpty class="text-muted-foreground">No results</ComboboxEmpty>
<ComboboxGroup>
<template v-for="member in members" :key="member.member_id">
<ComboboxItem :value="member"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5">
{{ member.member_name }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
</ComboboxItem>
</template>
</ComboboxGroup>
</ComboboxList>
</Combobox>
<!-- Status Combobox -->
<Combobox v-model="currentStatus">
<ComboboxAnchor class="w-[300px]">
<ComboboxInput placeholder="Search statuses..." class="w-full pl-9"
:display-value="(v) => v ? v.name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-[300px]">
<ComboboxEmpty class="text-muted-foreground">No results</ComboboxEmpty>
<ComboboxGroup>
<template v-for="status in statuses" :key="status.id">
<ComboboxItem :value="status"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5">
{{ status.name }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
</ComboboxItem>
</template>
</ComboboxGroup>
</ComboboxList>
</Combobox>
<!-- rank -->
<Combobox v-model="currentRank">
<ComboboxAnchor class="w-[300px]">
<ComboboxInput placeholder="Search ranks..." class="w-full pl-9"
:display-value="(v) => v ? v.short_name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-[300px]">
<ComboboxEmpty class="text-muted-foreground">No results</ComboboxEmpty>
<ComboboxGroup>
<template v-for="rank in allRanks" :key="rank.id">
<ComboboxItem :value="rank"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5">
{{ rank.short_name }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
</ComboboxItem>
</template>
</ComboboxGroup>
</ComboboxList>
</Combobox>
<Button :onClick="() => { }">Submit</Button>
</div>
</template>

View File

@@ -11,22 +11,24 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { computed, ref } from "vue";
import { Member, getMembers } from "@/api/member";
import { useRouter } from 'vue-router';
import { Ellipsis } from "lucide-vue-next";
import Input from "@/components/ui/input/Input.vue";
import LoaForm from "@/components/loa/loaForm.vue";
const members = ref<Member[]>([]);
const router = useRouter();
@@ -46,9 +48,24 @@ const searchedMembers = computed(() => {
return members.value.filter(member => member.member_name.toLowerCase().includes(searchVal.value.toLowerCase()));
});
// page state systems
const showLOADialog = ref(false);
const LOAuserId = ref<number | null>(null);
</script>
<template>
<Dialog v-model:open="showLOADialog" v-on:update:open="showLOADialog = false">
<DialogContent>
<DialogHeader>
<DialogTitle>LOA Menu</DialogTitle>
<DialogDescription>
Something something flavor text.
</DialogDescription>
</DialogHeader>
<LoaForm :adminMode="true"></LoaForm>
</DialogContent>
</Dialog>
<!-- table menu -->
<div class="w-4xl mx-auto">
<div class="flex justify-between mb-4">
@@ -72,6 +89,8 @@ const searchedMembers = computed(() => {
</TableCell>
<TableCell>{{ member.rank }}</TableCell>
<TableCell>{{ member.status }}</TableCell>
<TableCell>{{ member.status }}</TableCell>
<TableCell>{{ member.status }}</TableCell>
<TableCell @click.stop="console.log('hi')" class="text-right">
<DropdownMenu>
<DropdownMenuTrigger class="cursor-pointer">
@@ -80,7 +99,7 @@ const searchedMembers = computed(() => {
<DropdownMenuContent>
<DropdownMenuItem>Change Rank</DropdownMenuItem>
<DropdownMenuItem>Transfer</DropdownMenuItem>
<DropdownMenuItem>LOA</DropdownMenuItem>
<DropdownMenuItem @click="showLOADialog = true">LOA</DropdownMenuItem>
<DropdownMenuItem :variant="'destructive'">Retire</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -1,18 +1,35 @@
import { createRouter, createWebHistory } from 'vue-router'
import ManageApplications from '@/pages/ManageApplications.vue'
import Application from '@/pages/Application.vue'
import RankChange from '@/pages/RankChange.vue'
import MemberList from '@/pages/memberList.vue'
import LOA from '@/pages/LOA.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/applications', component: ManageApplications },
{ path: '/applications/:id', component: Application },
{ path: '/changeRank', component: RankChange },
{ path: '/members', component: MemberList},
{ path: '/loa', component: LOA}
{ 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: '/administration',
children: [
{
path: 'applications',
component: () => import('@/pages/ManageApplications.vue')
},
{
path: 'applications/:id',
component: () => import('@/pages/Application.vue')
},
{
path: 'transfer-requests',
component: () => import('@/pages/RankChange.vue')
},
{
path: 'loa',
component: () => import('@/pages/ManageLOA.vue')
}
]
}
]
})

10
ui/src/stores/user.ts Normal file
View File

@@ -0,0 +1,10 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const roles = ref<string[]>([])
const isLoggedIn = computed(() => user.value !== null)
return { user, isLoggedIn, roles }
})