Compare commits
13 Commits
1.0.0
...
training-r
| Author | SHA1 | Date | |
|---|---|---|---|
| e04cd1a110 | |||
| a52f5cd31a | |||
| 46d1a0c286 | |||
| 6c2b88352d | |||
| 71f9240088 | |||
| e35b61d06b | |||
| dc3430aa2e | |||
| ff5371d867 | |||
| f3e35f3f6a | |||
| d7b099ac75 | |||
| a6b521a89c | |||
| ad4d28b5dd | |||
| ac22e36202 |
20
api/package-lock.json
generated
20
api/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sentry/node": "^10.27.0",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"chalk": "^5.6.2",
|
||||
"connect-sqlite3": "^0.9.16",
|
||||
"cors": "^2.8.5",
|
||||
@@ -758,7 +759,6 @@
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
@@ -778,7 +778,6 @@
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
@@ -790,7 +789,6 @@
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
|
||||
"integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -799,6 +797,15 @@
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-session": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz",
|
||||
"integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
@@ -809,14 +816,12 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/morgan": {
|
||||
@@ -871,21 +876,18 @@
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
|
||||
"integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -895,7 +897,6 @@
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz",
|
||||
"integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
@@ -907,7 +908,6 @@
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
|
||||
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^10.27.0",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"chalk": "^5.6.2",
|
||||
"connect-sqlite3": "^0.9.16",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -12,7 +12,7 @@ const pool = mariadb.createPool({
|
||||
connectionLimit: 5,
|
||||
connectTimeout: 10000, // give it more breathing room
|
||||
acquireTimeout: 15000,
|
||||
database: 'ranger_unit_tracker',
|
||||
database: process.env.DB_DATABASE,
|
||||
ssl: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -55,21 +55,27 @@ if (process.env.DISABLE_GLITCHTIP === "true") {
|
||||
|
||||
//session setup
|
||||
import path = require('path');
|
||||
// import session = require('express-session');
|
||||
import session = require('express-session');
|
||||
import passport = require('passport');
|
||||
const SQLiteStore = require('connect-sqlite3')(session);
|
||||
|
||||
app.use(session({
|
||||
const cookieOptions: session.CookieOptions = {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
domain: process.env.CLIENT_DOMAIN,
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30, //30 days
|
||||
}
|
||||
const sessionOptions: session.SessionOptions = {
|
||||
secret: 'whatever',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: new SQLiteStore({ db: 'sessions.db', dir: './' }),
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
domain: process.env.CLIENT_DOMAIN
|
||||
}
|
||||
}));
|
||||
rolling: true,
|
||||
cookie: cookieOptions
|
||||
}
|
||||
|
||||
app.use(session(sessionOptions));
|
||||
app.use(passport.authenticate('session'));
|
||||
|
||||
// Mount route modules
|
||||
|
||||
@@ -46,32 +46,35 @@ 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]);
|
||||
let memberId: number;
|
||||
let memberId: number | null = null;
|
||||
//if member exists
|
||||
if (existing.length > 0) {
|
||||
memberId = existing[0].id;
|
||||
} else {
|
||||
//otherwise: create account
|
||||
//otherwise: create account mode
|
||||
const jwt = parseJwt(jwtClaims);
|
||||
const discordID = jwt.discord.id as number;
|
||||
const discordID = jwt.discord?.id as number;
|
||||
|
||||
//check if account is available to claim
|
||||
memberId = await mapDiscordtoID(discordID);
|
||||
if (discordID)
|
||||
memberId = await mapDiscordtoID(discordID);
|
||||
|
||||
if (memberId === null) {
|
||||
// create new account
|
||||
if (discordID && memberId) {
|
||||
// claim account
|
||||
console.log("Claiming account");
|
||||
const result = await con.query(
|
||||
`UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`,
|
||||
[sub, issuer, memberId]
|
||||
)
|
||||
} else {
|
||||
console.log("New Account");
|
||||
// new account
|
||||
const username = sub.username;
|
||||
const result = await con.query(
|
||||
`INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`,
|
||||
[username, sub, issuer]
|
||||
)
|
||||
memberId = Number(result.insertId);
|
||||
} else {
|
||||
// claim existing account
|
||||
const result = await con.query(
|
||||
`UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`,
|
||||
[sub, issuer, memberId]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,11 +118,24 @@ router.get('/callback', (req, res, next) => {
|
||||
router.get('/logout', [requireLogin], function (req, res, next) {
|
||||
req.logout(function (err) {
|
||||
if (err) { return next(err); }
|
||||
var params = {
|
||||
client_id: process.env.AUTH_CLIENT_ID,
|
||||
returnTo: process.env.CLIENT_URL
|
||||
};
|
||||
res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params));
|
||||
|
||||
req.session.destroy((err) => {
|
||||
if (err) { return next(err); }
|
||||
|
||||
res.clearCookie('connect.sid', {
|
||||
path: '/',
|
||||
domain: process.env.CLIENT_DOMAIN,
|
||||
httpOnly: true,
|
||||
sameSite: 'lax'
|
||||
});
|
||||
|
||||
var params = {
|
||||
client_id: process.env.AUTH_CLIENT_ID,
|
||||
returnTo: process.env.CLIENT_URL
|
||||
};
|
||||
res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params));
|
||||
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -83,8 +83,10 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise<numb
|
||||
try {
|
||||
var con = await pool.getConnection();
|
||||
|
||||
let course: Course = await getCourseByID(event.course_id);
|
||||
|
||||
await con.beginTransaction();
|
||||
const res = await con.query("INSERT INTO course_events (course_id, event_date, remarks, created_by) VALUES (?, ?, ?, ?);", [event.course_id, toDateTime(event.event_date), event.remarks, event.created_by]);
|
||||
const res = await con.query("INSERT INTO course_events (course_id, event_date, remarks, created_by, hasBookwork, hasQual) VALUES (?, ?, ?, ?, ?, ?);", [event.course_id, toDateTime(event.event_date), event.remarks, event.created_by, course.hasBookwork, course.hasQual]);
|
||||
var eventID: number = res.insertId;
|
||||
|
||||
for (const attendee of event.attendees) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function getUserRoles(userID: number): Promise<Role[]> {
|
||||
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;`;
|
||||
WHERE mr.member_id = ?;`;
|
||||
|
||||
return await pool.query(sql, [userID]);
|
||||
}
|
||||
@@ -197,7 +197,7 @@ defineExpose({ forceReload })
|
||||
<DropdownMenuItem v-if="activeEvent.cancelled" @click="setCancel(false)">
|
||||
Un-Cancel
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem v-else @click="setCancel(true)">
|
||||
<DropdownMenuItem v-else @click="setCancel(true)" class="text-destructive">
|
||||
Cancel
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -80,6 +80,19 @@ function onSubmit(vals) {
|
||||
}
|
||||
}
|
||||
|
||||
function matchesSearch(text: string, search: string) {
|
||||
if (!search) return true
|
||||
|
||||
const tokens = search
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
|
||||
const target = text.toLowerCase()
|
||||
|
||||
return tokens.every(token => target.includes(token))
|
||||
}
|
||||
|
||||
const { remove, push, fields } = useFieldArray('attendees');
|
||||
|
||||
const selectedCourse = computed<Course | undefined>(() => { return trainings.value?.find(c => c.id == values.course_id) })
|
||||
@@ -110,11 +123,24 @@ const memberSearch = ref('')
|
||||
const MAX_RESULTS = 50
|
||||
|
||||
const filteredMembers = computed(() => {
|
||||
const q = memberSearch?.value?.toLowerCase() ?? ""
|
||||
const q = memberSearch.value?.toLowerCase().trim() ?? ""
|
||||
|
||||
if (!q) {
|
||||
return (members.value ?? []).slice(0, MAX_RESULTS)
|
||||
}
|
||||
|
||||
// Split search into words (handles multiple spaces)
|
||||
const tokens = q.split(/\s+/)
|
||||
|
||||
const results: MemberLight[] = []
|
||||
|
||||
for (const m of members.value ?? []) {
|
||||
if (!q || (m.displayName || m.username).toLowerCase().includes(q)) {
|
||||
const name = (m.displayName || m.username).toLowerCase()
|
||||
|
||||
// ALL tokens must be present (order does not matter)
|
||||
const matches = tokens.every(token => name.includes(token))
|
||||
|
||||
if (matches) {
|
||||
results.push(m)
|
||||
if (results.length >= MAX_RESULTS) break
|
||||
}
|
||||
@@ -123,6 +149,17 @@ const filteredMembers = computed(() => {
|
||||
return results
|
||||
})
|
||||
|
||||
const courseSearch = ref('')
|
||||
|
||||
const filteredCourses = computed(() => {
|
||||
if (!trainings.value) return []
|
||||
|
||||
return trainings.value.filter(course =>
|
||||
matchesSearch(course.name, courseSearch.value)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<form id="trainingForm" @submit.prevent="submitForm" class="flex flex-col gap-5">
|
||||
@@ -139,18 +176,22 @@ const filteredMembers = computed(() => {
|
||||
selectCourse = false
|
||||
}" class="w-full">
|
||||
<ComboboxAnchor class="w-full">
|
||||
<ComboboxInput @focus="selectCourse = true" placeholder="Search courses..."
|
||||
class="w-full pl-3" :display-value="(id) => {
|
||||
<ComboboxInput @focus="selectCourse = true"
|
||||
@input="courseSearch = $event.target.value"
|
||||
placeholder="Search courses..."
|
||||
class="w-full pl-3"
|
||||
:display-value="(id) => {
|
||||
const c = trainings?.find(t => t.id === id)
|
||||
return c ? c.name : '';
|
||||
}" />
|
||||
}"
|
||||
/>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<ComboboxList class="w-full">
|
||||
<ComboboxEmpty class="text-muted-foreground w-full">No results</ComboboxEmpty>
|
||||
<ComboboxGroup>
|
||||
<div class="max-h-80 overflow-y-scroll scrollbar-themed min-w-md">
|
||||
<template v-for="course in trainings" :key="course.id">
|
||||
<template v-for="course in filteredCourses" :key="course.id">
|
||||
<ComboboxItem :value="course.id"
|
||||
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5 w-full">
|
||||
{{ course.name }}
|
||||
|
||||
Reference in New Issue
Block a user