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
}
:global(body) {
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")
if (request.ok) {
const currentUser = await request.text()
if (currentUser != "") {
loggedIn = true
}
}
})
onMount(async () => {
const request = await fetch('/api/v1/me');
if (request.ok) {
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
}
:global(body) {
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue,
helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
}
</style>

View file

@ -1,118 +1,117 @@
<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')
if (loggedIn.ok) {
login = false
}
const request = await fetch(`/api/v1/invite/${code}`)
if (request.ok) {
invite = await request.json()
}
})
const acceptInvite = async () => {
const response = await fetch(`/api/v1/invite/${code}/accept`, {
method: "POST",
});
if (response.ok) {
success = await response.text
} else {
console.log("not ok")
}
}
onMount(async () => {
const loggedIn = await fetch('/api/v1/me');
if (loggedIn.ok) {
login = false;
}
const request = await fetch(`/api/v1/invite/${code}`);
if (request.ok) {
invite = await request.json();
}
});
const acceptInvite = async () => {
const response = await fetch(`/api/v1/invite/${code}/accept`, {
method: 'POST'
});
if (response.ok) {
success = await response.text;
} else {
console.log('not ok');
}
};
</script>
<svelte:head>
{#if invite}
<title>Accept Invite to {invite.server.name}</title>
{:else}
<title>Accept Invite</title>
{/if}
{#if invite}
<title>Accept Invite to {invite.server.name}</title>
{:else}
<title>Accept Invite</title>
{/if}
</svelte:head>
<div>
{#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>
{:else}
<button on:click="{acceptInvite}">Yes!</button>
{/if}
{:else}
<p>You have been whitelisted! Connect at: <code>{invite.server.address}</code></p>
{/if}
{:else}
<p>Loading...</p>
{/if}
{#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>
{:else}
<button on:click={acceptInvite}>Yes!</button>
{/if}
{:else}
<p>You have been whitelisted! Connect at: <code>{invite.server.address}</code></p>
{/if}
{:else}
<p>Loading...</p>
{/if}
</div>
<style>
@import "https://fontlibrary.org/face/minecraftia";
div {
background-color: #444a52f6;
color: white;
margin: 0 auto;
width: 25vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 10px;
border-radius: 5px;
border: 1px solid white;
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
}
@media (max-width: 700px) {
div {
width: 90vw;
}
}
button {
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;
outline: 2px solid #000;
}
@import 'https://fontlibrary.org/face/minecraftia';
div {
background-color: #444a52f6;
color: white;
margin: 0 auto;
width: 25vw;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 10px;
border-radius: 5px;
border: 1px solid white;
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
}
@media (max-width: 700px) {
div {
width: 90vw;
}
}
button {
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;
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;
}
code {
font-family: 'MinecraftiaRegular';
color: white;
padding: 20px 5px;
margin: 20px 0;
display: block;
text-align: center;
font-size: larger;
background-color: black;
border: 3px inset grey;
}
:global(body) {
font-family: 'MinecraftiaRegular';
background-color: #202225;
background-image: url("/dirt.jpg");
}
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;
}
code {
font-family: 'MinecraftiaRegular';
color: white;
padding: 20px 5px;
margin: 20px 0;
display: block;
text-align: center;
font-size: larger;
background-color: black;
border: 3px inset grey;
}
:global(body) {
font-family: 'MinecraftiaRegular';
background-color: #202225;
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")
if (request.ok) {
loggedIn = true
} else {
goto("/")
}
})
onMount(async () => {
const request = await fetch('/api/v1/me');
if (request.ok) {
loggedIn = true;
} else {
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,39 +1,52 @@
<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")
if (request.ok) {
loaded = true
servers = await request.json()
} else {
console.log("not ok")
}
})
onMount(async () => {
const request = await fetch('/api/v1/servers');
if (request.ok) {
loaded = true;
servers = await request.json();
} 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);
}
};
</script>
{#if loaded}
<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}
<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}">view</a>
<button on:click={deleteServer} data-serverid="{server.id}">delete</button>
</div>
{/each}
{/if}
{:else}
<p>Loading</p>
<p>Loading</p>
{/if}
<style>
div {
margin: 0 auto;
}
div {
margin: 0 auto;
}
</style>

View file

@ -1,57 +1,64 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { goto } from '$app/navigation';
const createServer = async (e: Event) => {
const form: HTMLFormElement = e.currentTarget;
const url = form.action;
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
}
};
const pl = JSON.stringify(payload);
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
}
};
const pl = JSON.stringify(payload);
const response = await fetch("/api/v1/servers", {
method: "POST",
body: pl
});
if (response.ok) {
goto("/servers")
} else {
console.log("not ok")
}
}
const response = await fetch('/api/v1/servers', {
method: 'POST',
body: pl
});
if (response.ok) {
goto('/servers');
} else {
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>
label {
display: block;
margin: 10px 0;
}
input {
border: 1px solid black;
padding: 5px;
border-radius: 5px;
font-size: large;
}
label {
display: block;
margin: 10px 0;
}
input {
border: 1px solid black;
padding: 5px;
border-radius: 5px;
font-size: large;
}
</style>