Expanding admin interface to include invite logs, deletion

This commit is contained in:
Gabriel Simmer 2022-07-06 15:09:23 +01:00
parent a4f2a930b9
commit c02896b595
10 changed files with 418 additions and 275 deletions

View file

@ -1,8 +1,16 @@
<slot></slot>
<slot />
<footer>
<a href="/datacollection">Data Collection</a>
</footer>
<style>
:global(body) {
font-family:-apple-system,BlinkMacSystemFont,avenir next,avenir,segoe ui,helvetica neue,helvetica,Cantarell,Ubuntu,roboto,noto,arial,sans-serif
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue,
helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
}
footer {
position: absolute;
bottom: 0;
}
</style>

View file

@ -0,0 +1,22 @@
<h1>Data Collection</h1>
<p>
In order to operate, this service collects a small amount of user data. Should you want to
exercise your
<a href="https://gdpr.eu/right-to-be-forgotten/">right to be forgotten</a> or any other inquiries,
please reach out to me at mc-invites@gmem.ca.
</p>
<p>
User data collected includes your Minecraft username at time of signup, your unique Minecraft
UUID, and Microsoft OAuth tokens required to speak to Mojang's API. IP addresses or other
personally identifiable data that may be sent is not permenantly logged. Other data includes the
server-related configuration when adding a new server to create invites, which includes the
address of the server, the address of the RCON server, and the RCON password, in addition to a
custom server name.
</p>
<style>
p {
max-width: 50vw;
}
</style>

View file

@ -1,31 +1,32 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { goto } from '$app/navigation';
import { onMount } from "svelte";
import { onMount } from 'svelte';
let loggedIn = false
let loggedIn = false;
onMount(async () => {
const request = await fetch("/api/v1/me")
onMount(async () => {
const request = await fetch('/api/v1/me');
if (request.ok) {
const currentUser = await request.text()
if (currentUser != "") {
loggedIn = true
const currentUser = await request.text();
if (currentUser != '') {
loggedIn = true;
}
}
})
});
</script>
{#if !loggedIn}
<a href="/api/v1/auth/redirect">Login</a>
<a href="/api/v1/auth/redirect">Login</a>
{:else}
<a href="/servers">Manage Servers</a>
<a href="/servers">Manage Servers</a>
{/if}
<slot></slot>
<slot />
<style>
:global(body) {
font-family:-apple-system,BlinkMacSystemFont,avenir next,avenir,segoe ui,helvetica neue,helvetica,Cantarell,Ubuntu,roboto,noto,arial,sans-serif
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue,
helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
}
</style>

View file

@ -1,36 +1,35 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from "svelte";
import mslogin from "$lib/images/mslogin.png"
import { page } from '$app/stores';
import { onMount } from 'svelte';
import mslogin from '$lib/images/mslogin.png';
const { code } = $page.params;
const { code } = $page.params;
let invite;
let success = "";
let login = true;
let invite;
let success = '';
let login = true;
onMount(async () => {
const loggedIn = await fetch('/api/v1/me')
onMount(async () => {
const loggedIn = await fetch('/api/v1/me');
if (loggedIn.ok) {
login = false
login = false;
}
const request = await fetch(`/api/v1/invite/${code}`)
const request = await fetch(`/api/v1/invite/${code}`);
if (request.ok) {
invite = await request.json()
invite = await request.json();
}
})
});
const acceptInvite = async () => {
const acceptInvite = async () => {
const response = await fetch(`/api/v1/invite/${code}/accept`, {
method: "POST",
method: 'POST'
});
if (response.ok) {
success = await response.text
success = await response.text;
} else {
console.log("not ok")
console.log('not ok');
}
}
};
</script>
<svelte:head>
@ -42,25 +41,25 @@ const acceptInvite = async () => {
</svelte:head>
<div>
{#if invite}
{#if success == ""}
{#if invite}
{#if success == ''}
<p>Invite {code} from {invite.creator.display_name}</p>
<p>Join {invite.server.name}?</p>
{#if login }
<a href="/api/v1/auth/redirect"><img src="{mslogin}" alt="Login with Microsoft"></a>
{#if login}
<a href="/api/v1/auth/redirect"><img src={mslogin} alt="Login with Microsoft" /></a>
{:else}
<button on:click="{acceptInvite}">Yes!</button>
<button on:click={acceptInvite}>Yes!</button>
{/if}
{:else}
{:else}
<p>You have been whitelisted! Connect at: <code>{invite.server.address}</code></p>
{/if}
{:else}
<p>Loading...</p>
{/if}
{/if}
{:else}
<p>Loading...</p>
{/if}
</div>
<style>
@import "https://fontlibrary.org/face/minecraftia";
@import 'https://fontlibrary.org/face/minecraftia';
div {
background-color: #444a52f6;
color: white;
@ -84,20 +83,20 @@ const acceptInvite = async () => {
}
}
button {
color:white;
color: white;
width: 50%;
margin: 0 auto;
height: 6vh;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAAAPCAAAAACe4j/AAAAEuElEQVRIx1WWS5Ibuw5EtddrdRRJJE4iSKrb4c2/AUuy30iK+jEBHCTwAICMCgAKiGhKVQUwKzJHQo4mQGBy7AIvk1E2AQKhFt6bUGD1gILy0DkD4dmzACkGyzCScgFgQmLZr4LpQpC56QB2wgSYE8i1fd6xd1yPHIAKwAs14Undx2LIHK0X6vdFbzTa6CNBLQE8Y5rapn7oAl3KSDKC6cLfL6sLnIVG7ffnyUgvQ6rwLjUZdIVqWz0iEXXCZ+TJsV8feUdoPHDk+a/IeAbq0ldLT59jtHweHwmKS6gFSUYCqKcVsPami1pEnqolkCMBiAS87FXTICp03geYUr8TV8un3KuyC0BfTUzihOCdwjUya1bkT9VWe+AaFOoJzHdtERXRe/eulyEDhC6pjYwr+JQuIDU9/0wQBnJEAkd+9i6IRTJrV7YWnGfwLA6ESphkBoCu88M0YH1CTTOiRUpDdwpGkkP5yL9phhwn+QVxDarAXq513y9bLKgxBGR/CjDyMl4ECd6lO82CaMLbEHGHp1R8ePDERj392ssaCOigNjJ+dXXl6OLUFfVx6dA1BFJLlHpkUHPam095C3QJLIjn6OLIOB3xe0JS85Zd+govg1/z/UhtG3RdVW98yxNp5I0Vtf/U+6y/dJ/yHtSK4q7G9ARscC3/Df/kXTwiT3d6ovjEISLJIVEvBwWeCXlALZTbpy85xqWuf3sNiBBUmYjI+BqN6aNQqDWpipSYf3W1nOV/WALoykjIiCPreSn97UigsjZdPA5s3AmCDIxaVO0Fa1fV/qkaiijPPz/bEeRyLSBDI8K8j37TO8aH7vmOqUUQ1DboYhvqd+FlFGEo+2T75MbLGnjvPeu7Pl521Ap14dcR3h7p75qv4xieqnNDoyCPLt/yKkf5gOUXl+6S3d9Syq/982fz+fhpsm28C5EhJcxsWWQke6KoPMA7tqm7kKcrn6FLhWcoqCNCxDPyQ8wEKR73nIlEX8GpARSJX7/rzeNtWPD/Dl2IwtOYMwV7osGsHYJanzeU7KqIZVCTcrT0IEbyD4d1e1dkBtW2qLhLXK7a1ogDqsjI1DR4PY7nHEqfF9tn8HUJL+eQ/lGOXdwI1++57CqgmDaReM9CZYLAk4D4ymqti4KZI9Htz9OnrcuOhCQEqJfXaxHPAFG/l/ee1GFJPXmPryYUiUGPutnRGXkagmnjHk5RNpm7xxiclm6CjJEQ4+MD4xPmuT9Ef4b3YT3B51a7d4Np76PF0cavuC+SoaH6PlilipEakoJBJETWT3k7Hf+1REPj2fIx95t5ct6u9S5v3KM2fv2Ve29ax9yC98zT2IZot61MHyYtdPF92nguZ29xfcVxSu9Nj5Gl8G3wcVoW8n1Q/f6BHFEAyy5R2aRIdYH8XfV4453vHKcnqarXPD6mdrD0rlVV5xBPF2xuJM5Oo0SdaaqGMjJva7ii6bYv74IzPMo5pGebpu72ghypwTR+WWDuhQOkFijGyAzF3GXh11lEH0hkPINZivpTqLEtVm2j1hVcn/S0//q0es5dbyxOErxKivQ+bHotvzcT3/XztPf20XqG6fJZMePT2X6ts+nlm9LoSYHMiHuJjGXniB7v2fo/5qT4nuxYg6AAAAAASUVORK5CYII=);
font-family: 'MinecraftiaRegular';
border-color: #AAA #565656 #565656 #AAA;
text-shadow: 3px 3px #4C4C4C;
border-color: #aaa #565656 #565656 #aaa;
text-shadow: 3px 3px #4c4c4c;
outline: 2px solid #000;
}
button:hover {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAAAPCAMAAACMV5AuAAAAVFBMVEVtd7ZteLZverdwerdwe7hxfLhyfbl0frl1f7p2gLt4grx6hLx7hb18hr5+iL+AisCBi8GCjMGEjsKGkMOHkcSIksSKk8WLlMWLlcaMlcaNlsePmMdwBHmVAAAEjElEQVRIx1WW27YbKw5F3Ul2GYTWFAJX+pyd///PfgDsTg0/mSqQ1k08AEAeDkAA7kXyDAeyu2QCWREgCGSvAAbIE/D9U2m8Ji4Drw0CEhP7EMiqBFxmDMBEkHvd5AziDsjoCKQXddUl1nuZgMZrf0PMdj1kgNY+Ay+CJPg8kj1rR3X/GS9kxaoJVFaB6QkxIX5TBX65mpAZGQH3HaoCFMj6/GxuYgDyTsyup1hfxwxVN+F08ANGANyf8jqAPaBpranJvhxV+VV08JGPVScmcL8W4EIuAFWFHMZrUkUfNC3WtIsEaFogMWJRl839MES61w1cjMUsI1fb7GKaoENMOdFNykzXPz2mlwekkXjV4ioO9b15rZWZd4AaCL9cxdSutl8buIOUkd8JTgAy01IKoFoFPhAZr65SbDHgxFIGKi5IpLaKdlafAHxaVWDt2SSZ24LQhEx6aLGyj7RtELDLiA7EiD72ehBiQJo5oPq1nOQMYGAIYoY2AwIrImaA+25P8vZRa0KgqrjnQIaACiom/1G9Sla3GUHVLl/mMl/mxeUPGT0T5seAeeAQtF9WV2F7cX4niJ4DYmBd12oh7jymyRkBuq6M81mQuExHVv313beo9VH3ojfM2zZ8AkTG8X7EeDs6OZw+mrY7E5l/gqQJmZx+YySQAi1/dqQZy0as4FL1d7nrcfd1kjWTX1ZIYESTUCnuPZE7+anrqcxlz3diUX2HowHCf15S3AvxrnhRxeMwtQACGYGKZ7wGjNl7vH73MHdP8s/vSXM0FiRqMnPowfbjRsfe6n7nZ/FGIyfgF6+A/t1XTrcWB3Q42AxkMOfMfuJotRPgqGqFMfB8KO6eN+BEpO8F2eLZ18YO0GUdmEDcXMcNey+XuOfvP5P35guSVxAzWRYUpIoSuZiJrJ/p4zPouw0pwX+6XwpIcyNWEcJ/uf5Svuyx+ncTfhmotEhIBPe/8f961F/jI5aQfSs3WFOwChnZZxPk+HQjZu/WBuDFVwobZnoLISBOdkmNLFPE9iadiImsgTcdTDKA8dj1qBT518Xcg6+6L0Yl/nqCLeH+b46InkCSETTBzEQBRoPEwS9lKVUkpEx426M3oIMHYQLhK/B6jHvgXw6if4+YM4mlJdWDpLw4MhHgjwBZgIt+ZJdAVENOEkiv6mY4yy5nFPjnQmGfGVy0IqP+cmbsWF7Se/rT+5ZBzHPdKfajHW2ouWldPJaoTDJ3GUYTNMV/IyaiPYqQuX0VPXK+ra3Mc8c5Wlkp4D/e5S5htjXU3HnPPJsBXraRM/rSpOMX99zMoVradRl9AMxJdVPIYgf8Rhu0DvLM739A5rmjJ51QcW9SFXjcvR85oYOhSKTsd65pqdKWIV59ZO/7kKTDJLbvYw9XVRIyTXKJdfm4rOjE18yT8IHM/euZa0Z0fLvBSIh7h925QLoXR80WNfnqOHHP7Qk5aj+dTLX4E3hhIkafoGd15/IDT/lPTVSVr233dy6OkJtiLm0yBu+byeE2Yc59pVrDdKyPjxgD4h6r6vcFwpYNHMxVBaE2ApmfyTTjf5jQnWRymG7iAAAAAElFTkSuQmCC);
border-color: #BDC6FF #59639A #59639A #BDC6FF;
border-color: #bdc6ff #59639a #59639a #bdc6ff;
}
code {
font-family: 'MinecraftiaRegular';
@ -113,6 +112,6 @@ const acceptInvite = async () => {
:global(body) {
font-family: 'MinecraftiaRegular';
background-color: #202225;
background-image: url("/dirt.jpg");
background-image: url('/dirt.jpg');
}
</style>

View file

@ -1,58 +0,0 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from '$app/stores';
const { id } = $page.params;
let inviteCode = "";
const createInvite = async (e: Event) => {
const form: HTMLFormElement = e.currentTarget;
const url = form.action;
let formFields = new FormData(form);
let formData = Object.fromEntries(formFields.entries());
const payload = {
"server": id,
"uses": Number(formData.uses),
"unlimited": formData.unlimited == "on"
};
if (payload.uses === 0) { payload.uses = 5 }
console.log(payload)
const pl = JSON.stringify(payload);
const response = await fetch("/api/v1/invites", {
method: "POST",
body: pl
});
if (response.ok) {
inviteCode = await response.text()
} else {
console.log("not ok")
}
}
</script>
{#if inviteCode }
<code>{$page.url.hostname}/invite/{inviteCode}</code>
{:else}
<h2>Create Invite</h2>
<form on:submit|preventDefault="{createInvite}" action="/api/v1/invites" method="POST">
<label>Number of uses
<input type="number" name="uses" placeholder="5"></label>
<label>Unlimited?
<input type="checkbox" name="unlimited"></label>
<input type="submit" value="Save">
</form>
{/if}
<style>
label {
margin: 10px 0;
}
input {
border: 1px solid black;
padding: 5px;
border-radius: 5px;
font-size: large;
}
</style>

View file

@ -0,0 +1,92 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
const { id } = $page.params;
let server;
let invites;
let loaded = false;
onMount(async () => {
const request = await fetch(`/api/v1/server/${id}`);
if (request.ok) {
server = await request.json();
} else {
console.log('not ok');
}
const r2 = await fetch(`/api/v1/server/${id}/invites`);
if (request.ok) {
invites = await r2.json();
for (let invite in invites) {
const logsReq = await fetch(`/api/v1/invite/${invites[invite].token}/log`)
if (request.ok) {
invites[invite]["log"] = await logsReq.json()
}
}
loaded = true;
} else {
console.log('not ok');
}
});
const deleteServer = async (e: Event) => {
const element = e.target;
const serverId = element.dataset.serverid;
const request = await fetch(`/api/v1/server/${serverId}`, {
method: "DELETE"
});
if (request.ok) {
servers = servers.filter((server) => server.id != serverId);
}
};
const deleteInvite = async (e: Event) => {
const element = e.target;
const token = element.dataset.invitetoken;
const request = await fetch(`/api/v1/invite/${token}`, {
method: "DELETE"
});
if (request.ok) {
invites = invites.filter((invite) => invite.token != token);
}
};
</script>
{#if loaded}
<h2>{server.name}</h2>
<div>
<p>Server Address: {server.address}</p>
<p>Server RCON Address: {server.rcon.address}</p>
<div>
<h4>Invites <a href="/servers/{server.id}/new">create new</a></h4>
{#if invites.length > 0}
{#each invites as invite}
<details>
<summary>{invite.token} | Uses: {!invite.unlimited ? invite.uses : "unlimited"} <button on:click={deleteInvite} data-invitetoken="{invite.token}">delete</button></summary>
{#if invite.log}
{#each invite.log as log}
<p>{log.user.display_name} ({log.user.id})</p>
{/each}
{/if}
</details>
{/each}
{:else}
<p>No invites</p>
{/if}
</div>
<button on:click={deleteServer} data-serverid="{server.id}">delete</button>
</div>
{:else}
<p>Loading</p>
{/if}
<style>
div {
margin: 0 auto;
}
</style>

View file

@ -0,0 +1,59 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
const { id } = $page.params;
const createInvite = async (e: Event) => {
const form: HTMLFormElement = e.currentTarget;
const url = form.action;
let formFields = new FormData(form);
let formData = Object.fromEntries(formFields.entries());
const payload = {
server: id,
uses: Number(formData.uses),
unlimited: formData.unlimited == 'on'
};
if (payload.uses === 0) {
payload.uses = 5;
}
console.log(payload);
const pl = JSON.stringify(payload);
const response = await fetch('/api/v1/invites', {
method: 'POST',
body: pl
});
if (response.ok) {
goto(`/servers/${id}`)
} else {
console.log('not ok');
}
};
</script>
<h2>Create Invite</h2>
<form on:submit|preventDefault={createInvite} action="/api/v1/invites" method="POST">
<label
>Number of uses
<input type="number" name="uses" placeholder="5" /></label
>
<label
>Unlimited?
<input type="checkbox" name="unlimited" /></label
>
<input type="submit" value="Save" />
</form>
<style>
label {
margin: 10px 0;
}
input {
border: 1px solid black;
padding: 5px;
border-radius: 5px;
font-size: large;
}
</style>

View file

@ -1,22 +1,22 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { goto } from '$app/navigation';
import { onMount } from "svelte";
import { onMount } from 'svelte';
let loggedIn = false
let loggedIn = false;
onMount(async () => {
const request = await fetch("/api/v1/me")
onMount(async () => {
const request = await fetch('/api/v1/me');
if (request.ok) {
loggedIn = true
loggedIn = true;
} else {
goto("/")
goto('/');
}
})
});
</script>
{#if !loggedIn}
<a href="/api/v1/auth/redirect">Login</a>
<a href="/api/v1/auth/redirect">Login</a>
{/if}
<slot></slot>
<slot />

View file

@ -1,35 +1,48 @@
<script lang="ts">
import { server } from "$app/env";
import { onMount } from "svelte";
import { server } from '$app/env';
import { onMount } from 'svelte';
let servers = []
let loaded = false
let servers = [];
let loaded = false;
onMount(async () => {
const request = await fetch("/api/v1/servers")
onMount(async () => {
const request = await fetch('/api/v1/servers');
if (request.ok) {
loaded = true
servers = await request.json()
loaded = true;
servers = await request.json();
} else {
console.log("not ok")
console.log('not ok');
}
})
});
const deleteServer = async (e: Event) => {
const element = e.target;
const serverId = element.dataset.serverid;
const request = await fetch(`/api/v1/server/${serverId}`, {
method: "DELETE"
});
if (request.ok) {
servers = servers.filter((server) => server.id != serverId);
}
};
</script>
{#if loaded}
<h2>Your servers <small><a href="/servers/new">new</a></small></h2>
{#if servers.length > 0}
{#each servers as server}
<div>
<h2>Your servers <small><a href="/servers/new">new</a></small></h2>
{#if servers.length > 0}
{#each servers as server}
<div>
<h3>{server.name}</h3>
<p>Server Address: {server.address}</p>
<p>Server RCON Address: {server.rcon.address}</p>
<a href="/servers/{server.id}">new invite</a> <button>delete</button>
</div>
{/each}
{/if}
<a href="/servers/{server.id}">view</a>
<button on:click={deleteServer} data-serverid="{server.id}">delete</button>
</div>
{/each}
{/if}
{:else}
<p>Loading</p>
<p>Loading</p>
{/if}
<style>

View file

@ -1,46 +1,53 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { goto } from '$app/navigation';
const createServer = async (e: Event) => {
const createServer = async (e: Event) => {
const form: HTMLFormElement = e.currentTarget;
const url = form.action;
let formFields = new FormData(form);
let formData = Object.fromEntries(formFields.entries());
const payload = {
"address": formData.address,
"name": formData.name,
"rcon": {
"address": formData.rcon_address,
"password": formData.rcon_password
address: formData.address,
name: formData.name,
rcon: {
address: formData.rcon_address,
password: formData.rcon_password
}
};
const pl = JSON.stringify(payload);
const response = await fetch("/api/v1/servers", {
method: "POST",
const response = await fetch('/api/v1/servers', {
method: 'POST',
body: pl
});
if (response.ok) {
goto("/servers")
goto('/servers');
} else {
console.log("not ok")
console.log('not ok');
}
}
};
</script>
<h2>Add Server</h2>
<form on:submit|preventDefault="{createServer}" action="/api/v1/servers" method="POST">
<label>Server Name
<input type="text" name="name" placeholder="A Minecraft Server"></label>
<label>Server Address
<input type="text" name="address" placeholder="127.0.0.1"></label>
<label>RCON Address
<input type="text" name="rcon_address" placeholder="127.0.0.1:25575"></label>
<label>RCON Password
<input type="password" name="rcon_password" placeholder="rcon password"></label>
<input type="submit" value="Save">
<form on:submit|preventDefault={createServer} action="/api/v1/servers" method="POST">
<label
>Server Name
<input type="text" name="name" placeholder="A Minecraft Server" /></label
>
<label
>Server Address
<input type="text" name="address" placeholder="127.0.0.1" /></label
>
<label
>RCON Address
<input type="text" name="rcon_address" placeholder="127.0.0.1:25575" /></label
>
<label
>RCON Password
<input type="password" name="rcon_password" placeholder="rcon password" /></label
>
<input type="submit" value="Save" />
</form>
<style>