report: added

This commit is contained in:
Gxorge committed 2023-06-04 16:28:13 +01:00
1 parent 7027a8b72c
commit 483348ac17
30 files changed
+4858 -1

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
This contains the following websites:
- Staff panel
- Public Appeals
- Public Reports (soon)
- Public Reports
<br>
These all link up to the [Staff-OG](https://github.com/Gxorge/Staff-OG) plugin.
+10
View File
@@ -0,0 +1,10 @@
ORIGIN: https://yoursite.com
HOST: 0.0.0.0
PORT: 80
DB_HOST: localhost
DB_USER: staffog
DB_PWD: securepasswords
DB_NAME: staffog
TOKEN_SECRET: supersecurerandomlettersandnumbersdonttellanyonethis
+10
View File
@@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+2
View File
@@ -0,0 +1,2 @@
engine-strict=true
resolution-mode=highest
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
+4110
View File
File diff suppressed because it is too large. Load diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "staffog-report",
"version": "1.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-node": "^1.2.4",
"@sveltejs/kit": "^1.5.0",
"@types/bcryptjs": "^2.4.2",
"@types/jsonwebtoken": "^9.0.2",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.3.0"
},
"type": "module",
"dependencies": {
"bcryptjs": "^2.4.3",
"bulma": "^0.9.4",
"dotenv": "^16.0.3",
"jsonwebtoken": "^9.0.0",
"mariadb": "^3.1.2",
"mysql2": "^3.3.1",
"node-sass": "^8.0.0",
"pretty-ms": "^8.0.0",
"sass": "^1.62.1"
}
}
+12
View File
@@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+13
View File
@@ -0,0 +1,13 @@
@charset "utf-8";
$primary: #ff7582;
$menu-item-color: #ffffff;
$menu-item-hover-color: #ffffff;
$menu-item-hover-background-color: #355c7d;
body {
height: 100vh;
}
@import "../node_modules/bulma/bulma.sass";
+65
View File
@@ -0,0 +1,65 @@
import * as dotenv from "dotenv";
import * as mariadb from "mariadb";
dotenv.config();
export const dbPool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PWD,
database: process.env.DB_NAME,
multipleStatements: false
});
const inputAllowed = /^[a-zA-Z0-9_]+$/;
const inputAllowedEvidence = /^[a-zA-Z0-9_-]+$/;
export function checkInput(input: String): boolean {
return Boolean(input.match(inputAllowed));
}
export function checkInputEvidence(input: String): boolean {
return Boolean(input.match(inputAllowedEvidence));
}
export async function getNameFromUUID(uuid: string): Promise<string | null> {
let conn;
let name: string;
try {
conn = await dbPool.getConnection();
const result = await conn.query("SELECT `name` FROM `staffog_history` WHERE `uuid`=? ORDER BY `id` DESC LIMIT 1;", uuid);
name = result[0].name;
if (!name) return null;
} catch (e) {
console.log(e);
return null;
} finally {
if (conn) conn.release();
}
return name;
}
export async function getUUIDFromName(name: string): Promise<string | null> {
let conn;
let uuid: string;
try {
conn = await dbPool.getConnection();
const result = await conn.query("SELECT `uuid` FROM `staffog_history` WHERE `name`=? ORDER BY `id` DESC LIMIT 1;", name);
uuid = result[0].uuid;
if (!uuid) return null;
} catch (e) {
console.log(e);
return null;
} finally {
if (conn) conn.release();
}
return uuid;
}
+50
View File
@@ -0,0 +1,50 @@
import type { ChatReportEntry, ChatReportMessage, ReportEntry, ReportEvidence } from "$lib/types";
import { dbPool, getNameFromUUID } from "./global";
export async function getReport(uuid: String, id: number): Promise<ReportEntry | null> {
let conn;
try {
conn = await dbPool.getConnection();
let result = await conn.query("SELECT * FROM `staffog_report` WHERE `id`=? AND `by_uuid`=?;", [id, uuid]);
let entry = (result[0] as ReportEntry);
if (!entry) return null;
let name = await getNameFromUUID(entry.uuid);
if (name) entry.name = name;
let byName = await getNameFromUUID(entry.by_uuid);
if (byName) entry.by_name = byName;
if (entry.assigned) {
let assignedName = await getNameFromUUID(entry.assigned);
if (assignedName) entry.assigned_name = assignedName;
}
return entry;
} catch (e) {
console.log(e);
return null;
} finally {
if (conn) conn.release();
}
}
export async function submitReport(uuid: string, by_uuid: string, type: string, reason: string, evidence: Array<ReportEvidence>, crid: number | null): Promise<string | null> {
let conn;
try {
conn = await dbPool.getConnection();
let currentTime = new Date().getTime();
const result = await conn.query("INSERT INTO `staffog_report` (`uuid`, `by_uuid`, `time`, `type`, `reason`, `evidence`, `crid`) VALUES (?, ?, ?, ?, ?, ?, ?)", [uuid, by_uuid, currentTime, type, conn.escape(reason), JSON.stringify(evidence), crid]);
return result.insertId;
} catch (e) {
console.log(e);
return null;
} finally {
if (conn) conn.release();
}
}
+13
View File
@@ -0,0 +1,13 @@
import prettyMilliseconds from "pretty-ms";
export function getReadableMillis(until: bigint, time: bigint) {
if (until == BigInt(-1))
return "Permanent";
return prettyMilliseconds(Number(until) - Number(time), {verbose: true});
}
export function getReadableDate(time: bigint): string {
let date: Date = new Date(Number(time));
return date.toLocaleString();
}
+40
View File
@@ -0,0 +1,40 @@
export interface ReportEntry {
id: number,
uuid: string,
name: string,
by_uuid: string,
by_name: string,
time: bigint,
type: string,
crid: number | null,
reason: string,
open: number,
assigned: string,
assigned_name: string,
verdict: number,
verdict_time: bigint,
comment: string
}
export interface ChatReportMessage {
uuid: string,
name: string,
time: bigint,
message: string
}
export interface ChatReportEntry {
id: number,
uuid: string,
name: string,
by_uuid: string,
by_name: string,
time: bigint,
messages: string,
processedMessages: Array<ChatReportMessage>
}
export interface ReportEvidence {
type: String,
data: String
}
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts">
import { page } from '$app/stores';
import "$lib/app.scss";
</script>
<div class="content has-text-centered">
<h2>Error {$page.status}</h2>
<p>{$page.error.message}</p>
<br><br>
<p>If you believe this is a mistake, please get in touch with <b>admin@trueog.net</b>.</p>
</div>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
import "$lib/app.scss";
</script>
<main>
<container class="container box">
<div class="content has-text-centered">
<img src="/logo-alternate-transparent.png" alt="TrueOG logo." />
</div>
<slot/>
</container>
</main>
<style lang="scss">
img {
width: 50%;
height: auto;
}
</style>
+24
View File
@@ -0,0 +1,24 @@
<svelte:head>
<title>Reports | TrueOG</title>
</svelte:head>
<div class="content has-text-centered">
Welcome to TrueOG Reports. Please select an option below to continue.
</div>
<section class="section">
<div class="columns">
<div class="column">
<div class="content has-text-centered">
<p><b>Has someone broken the rules?</b></p>
<a class="button is-link" href="/submit">Submit an report</a>
</div>
</div>
<div class="column">
<div class="content has-text-centered">
<p><b>Need an update?</b></p>
<a class="button is-link" href="/check">Check your report's status</a>
</div>
</div>
</div>
</section>
+39
View File
@@ -0,0 +1,39 @@
import { checkInput, getUUIDFromName } from '$lib/server/global.js';
import { getReport } from '$lib/server/report.js';
import { fail } from '@sveltejs/kit';
export const actions = {
default: async (event) => {
const data = await event.request.formData();
let username = data.get('username');
let refForm = data.get('ref');
if (!username || !refForm) {
return fail(400, { success: false, message: "Please enter your username and reference number." });
}
username = username.toString();
if (!Number(refForm)) {
return fail(400, { success: false, message: "Please enter a valid reference number." });
}
let ref = Number(refForm);
if (!checkInput(username)) {
return fail(400, { success: false, message: "Please enter a valid username." });
}
let uuid = await getUUIDFromName(username);
if (!uuid) {
return fail(400, { success: false, message: "Please check your username, have you ever joined the server?" });
}
let report = await getReport(uuid, ref);
if (!report) {
return fail(400, { success: false, message: "Could not find your report. Please check your username and reference number." });
}
return { success: true, report: report, username: username }
}
}
+87
View File
@@ -0,0 +1,87 @@
<script lang="ts">
import type { ActionData } from "./$types";
export let form: ActionData;
</script>
<svelte:head>
<title>Check Report | TrueOG</title>
</svelte:head>
<form method="post">
<section class="section" style="padding-top: 0px;">
{#if form != null && !form.success}
<div class="notification is-danger">{form?.message}</div>
{/if}
{#if form == null || form.report == null}
<div class="content has-text-centered"><p>Please enter your username and reference number.</p></div>
<div class="field">
<label class="label">
IGN
<div class="control">
<input name="username" class="input " type="text" placeholder="Your IGN">
</div>
</label>
</div>
<div class="field">
<label class="label">
Refernce Number
<div class="control">
<input name="ref" class="input " type="text" placeholder="Your reference number">
</div>
</label>
</div>
<div class="control">
<button class="button is-link">Submit</button>
</div>
{:else}
<div class="content has-text-centered">
<h1>Report #{form.report.id} against {form.report.name}</h1>
<p>You have submitted a {form.report.type} report against {form.report.name}</p>
{#if form.report.open}
<br>
<p>Your report is still being investigated. Please check back later.</p>
{/if}
<table class="table is-bordered is-striped ">
<tbody>
<tr>
<th>Report Status</th>
<td>{form.report.open ? "Open" : "Closed"}</td>
</tr>
<tr>
<th>Your Comments</th>
<td>{form.report.reason}</td>
</tr>
{#if form.report.crid != null}
<tr>
<th>Chat Report ID</th>
<td>#{form.report.crid}</td>
</tr>
{/if}
{#if form.report.open}
<tr>
<th>Report Verdict</th>
<td>Pending</td>
</tr>
{:else}
<tr>
<th>Report Verdict</th>
<td>{form.report.verdict == 0 ? "No Futher Action" : "Punishment Issued"}</td>
</tr>
<tr>
<th>Staff Comment</th>
<td>{form.report.comment}</td>
</tr>
{/if}
</tbody>
</table>
</div>
{/if}
</section>
</form>
+13
View File
@@ -0,0 +1,13 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<svelte:head>
<title>Report Submitted | TrueOG</title>
</svelte:head>
<div class="content has-text-centered">
<h2>Report Submitted</h2>
<p>Thanks for your report! We have recieved it successfully and will review it as soon as possible. Your reference code is <b>{data.ref}</b>, you will need this to check the status of your report or if you need any further assistance.</p>
</div>
+8
View File
@@ -0,0 +1,8 @@
export const load = async ({ url }) => {
let ref = url.searchParams.get('ref');
return {
ref
};
}
+84
View File
@@ -0,0 +1,84 @@
import { checkInput, checkInputEvidence, getUUIDFromName } from '$lib/server/global.js';
import { submitReport } from '$lib/server/report.js';
import type { ReportEvidence } from '$lib/types.js';
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
submit: async (event) => {
const data = await event.request.formData();
let username = data.get("username");
let offender = data.get("offender");
let offence = data.get("offence");
let crid: FormDataEntryValue | null | Number = data.get("crid");
let reason = data.get("reason");
let evidenceAmount = data.get("evlinkamt");
let evidence: Array<ReportEvidence> = [];
if (!username || !offender || !offence || !reason || !evidenceAmount) {
return fail(500, { success: false, message: "Please complete the form." });
}
if (offence.toString() == "CHAT" && !crid) {
return fail(500, { success: false, message: "Please complete the form." });
}
if (crid && !Number(crid)) {
return fail(500, { success: false, message: "Chat Report ID must be a number." });
}
username = username.toString();
offender = offender.toString();
offence = offence.toString();
crid = Number(crid);
reason = reason.toString();
for (let i = 0; i<Number(evidenceAmount); i++) {
let eviType = data.get("evlink" + i + "-type");
let eviData = data.get("evlink" + i + "-data");
if (!eviType || !eviData) {
let allowNoEvi = event.cookies.get("allowNoEvi");
if (!allowNoEvi) {
event.cookies.set("allowNoEvi", "true");
return fail(500, { success: false, message: "Please complete the evidence boxes. If you created too many boxes, please press submit again and the form will be accepted." });
} else {
continue;
}
}
if (!checkInputEvidence(eviType.toString()) || !checkInputEvidence(eviData.toString())) {
return fail(500, { success: false, message: "Please check your evidence URLs, are they valid?" });
}
evidence.push({
type: eviType.toString(),
data: eviData.toString()
});
}
if (!checkInput(username) || !checkInput(offender)) {
return fail(500, { success: false, message: "Please check the usernames provided, are they valid?" });
}
let userUuid = await getUUIDFromName(username);
let offenderUuid = await getUUIDFromName(offender);
if (!userUuid) {
return fail(500, { success: false, message: "Please check your username, have you ever joined the server?" });
}
if (!offenderUuid) {
return fail(500, { success: false, message: "Please check the offender's username, have they ever joined the server?" });
}
event.cookies.delete("allowNoEvi");
let code = await submitReport(offenderUuid, userUuid, offence, reason, evidence, (!crid ? null : Number(crid)));
if (!code) {
return fail(500, { success: false, message: "Server failed to submit report, please try again later." });
}
throw redirect(303, "/done?ref=" + code);
},
}
+123
View File
@@ -0,0 +1,123 @@
<script lang="ts">
import { enhance } from "$app/forms";
import type { ActionData } from "./$types";
export let form: ActionData;
let selectedOffence: String;
let inGameFailed: boolean = false;
let evidenceUrls = 0;
</script>
<svelte:head>
<title>Submit Report | TrueOG</title>
</svelte:head>
<div class="content has-text-centered"><p><b>Report Submission</b></p></div>
<form method="post" use:enhance>
<section class="section" style="padding-top: 0px;">
{#if form != null && !form.success}
<div class="notification is-danger">{form?.message}</div>
{/if}
<div class="content has-text-centered"><p>Please fill out the below form to submit a report. Please include as much detail as possible, including any evidence required.</p></div>
<div class="field">
<label class="label">
What is your IGN?
<div class="control">
<input name="username" class="input " type="text" placeholder="Your IGN">
</div>
</label>
</div>
<div class="field">
<label class="label">
What is the offender's IGN?
<div class="control">
<input name="offender" class="input " type="text" placeholder="Offender's IGN">
</div>
</label>
</div>
<div class="field">
<label class="label">
What offence have they commited?
<div class="control">
<div class="select">
<select name="offence" bind:value={selectedOffence}>
<option disabled selected>Please select an offence</option>
<option value="CHAT">Mutable Offence (i.e. abuse in chat, such as death threats.)</option>
<option value="BAN">Bannable Offence (i.e cheating, exploiting, etc.)</option>
<option value="JAIL">Jailable Offence (i.e duping, glitching, etc.)</option>
</select>
</div>
</div>
</label>
</div>
{#if selectedOffence != null && selectedOffence == "CHAT" && inGameFailed == false}
<div class="notification is-danger">
Mutable Offences required an in-game chat report ID so that messages can be properly verified. This means that reports for Muteable Offences can only be submitted in-game with the <b>/chatreport</b> command.
<br>
If the in-game command failed but still gave you a chat report ID, you can continue with the button below.
<br>
<span class="button" on:click={() => {inGameFailed = true;}}>Continue with Chat Report ID</span>
</div>
{/if}
{#if selectedOffence != null && selectedOffence == "CHAT" && inGameFailed == true}
<div class="field">
<label class="label">
What is your chat report ID?
<div class="control">
<input name="crid" class="input " type="text" placeholder="Chat report ID">
</div>
</label>
</div>
{/if}
<div class="field">
<label class="label">
Please explain their offence. Evidence can be submitted below using the approved channels.
<div class="control">
<textarea name="reason" class="textarea" placeholder="Please explain the offence."/>
</div>
</label>
</div>
{#each {length: evidenceUrls} as _, i}
<span class="label">
Evidence Link #{i+1}
</span>
<div class="field has-addons">
<div class="contorl">
<span class="select">
<select name="evlink{i}-type">
<option value="YOUTUBE">youtube.com/watch?v=</option>
<option value="IMGUR">imgur.com/a/</option>
<option value="STREAMABLE">streamable.com/</option>
</select>
</span>
</div>
<div class="control">
<input name="evlink{i}-data" class="input " type="text" placeholder="Please finish the URL">
</div>
</div>
<div class="field has-addons">
</div>
{/each}
<input name="evlinkamt" type="text" value="{evidenceUrls}" hidden>
<span class="button is-link" on:click={() => {evidenceUrls++;}}>Add Evidence Link</span>
<br>
<br>
<div class="control">
<button class="button is-success" formaction="?/submit" disabled={selectedOffence != null && selectedOffence == "CHAT" && inGameFailed == false}>Submit</button>
</div>
</section>
</form>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+23
View File
@@ -0,0 +1,23 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
files: {
lib: "src/lib",
assets: "src/assets"
}
}
};
export default config;
+23
View File
@@ -0,0 +1,23 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"baseUrl": ".",
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}
+6
View File
@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});