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> <style>
:global(body) { :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> }
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"> <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 () => { onMount(async () => {
const request = await fetch("/api/v1/me") const request = await fetch('/api/v1/me');
if (request.ok) { if (request.ok) {
const currentUser = await request.text() const currentUser = await request.text();
if (currentUser != "") { if (currentUser != '') {
loggedIn = true loggedIn = true;
} }
} }
}) });
</script> </script>
{#if !loggedIn} {#if !loggedIn}
<a href="/api/v1/auth/redirect">Login</a> <a href="/api/v1/auth/redirect">Login</a>
{:else} {:else}
<a href="/servers">Manage Servers</a> <a href="/servers">Manage Servers</a>
{/if} {/if}
<slot></slot> <slot />
<style> <style>
:global(body) { :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> }
</style>

View file

@ -1,118 +1,117 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from "svelte"; import { onMount } from 'svelte';
import mslogin from "$lib/images/mslogin.png" import mslogin from '$lib/images/mslogin.png';
const { code } = $page.params; const { code } = $page.params;
let invite; let invite;
let success = ""; let success = '';
let login = true; let login = true;
onMount(async () => { onMount(async () => {
const loggedIn = await fetch('/api/v1/me') const loggedIn = await fetch('/api/v1/me');
if (loggedIn.ok) { 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) { if (request.ok) {
invite = await request.json() 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")
}
}
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> </script>
<svelte:head> <svelte:head>
{#if invite} {#if invite}
<title>Accept Invite to {invite.server.name}</title> <title>Accept Invite to {invite.server.name}</title>
{:else} {:else}
<title>Accept Invite</title> <title>Accept Invite</title>
{/if} {/if}
</svelte:head> </svelte:head>
<div> <div>
{#if invite} {#if invite}
{#if success == ""} {#if success == ''}
<p>Invite {code} from {invite.creator.display_name}</p> <p>Invite {code} from {invite.creator.display_name}</p>
<p>Join {invite.server.name}?</p> <p>Join {invite.server.name}?</p>
{#if login } {#if login}
<a href="/api/v1/auth/redirect"><img src="{mslogin}" alt="Login with Microsoft"></a> <a href="/api/v1/auth/redirect"><img src={mslogin} alt="Login with Microsoft" /></a>
{:else} {:else}
<button on:click="{acceptInvite}">Yes!</button> <button on:click={acceptInvite}>Yes!</button>
{/if} {/if}
{:else} {:else}
<p>You have been whitelisted! Connect at: <code>{invite.server.address}</code></p> <p>You have been whitelisted! Connect at: <code>{invite.server.address}</code></p>
{/if} {/if}
{:else} {:else}
<p>Loading...</p> <p>Loading...</p>
{/if} {/if}
</div> </div>
<style> <style>
@import "https://fontlibrary.org/face/minecraftia"; @import 'https://fontlibrary.org/face/minecraftia';
div { div {
background-color: #444a52f6; background-color: #444a52f6;
color: white; color: white;
margin: 0 auto; margin: 0 auto;
width: 25vw; width: 25vw;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
border: 1px solid white; border: 1px solid white;
position: absolute; position: absolute;
top: 30%; top: 30%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
@media (max-width: 700px) { @media (max-width: 700px) {
div { div {
width: 90vw; width: 90vw;
} }
} }
button { button {
color:white; color: white;
width: 50%; width: 50%;
margin: 0 auto; margin: 0 auto;
height: 6vh; 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=); 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'; font-family: 'MinecraftiaRegular';
border-color: #AAA #565656 #565656 #AAA; border-color: #aaa #565656 #565656 #aaa;
text-shadow: 3px 3px #4C4C4C; text-shadow: 3px 3px #4c4c4c;
outline: 2px solid #000; outline: 2px solid #000;
} }
button:hover { 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); 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 { code {
font-family: 'MinecraftiaRegular'; font-family: 'MinecraftiaRegular';
color: white; color: white;
padding: 20px 5px; padding: 20px 5px;
margin: 20px 0; margin: 20px 0;
display: block; display: block;
text-align: center; text-align: center;
font-size: larger; font-size: larger;
background-color: black; background-color: black;
border: 3px inset grey; border: 3px inset grey;
} }
:global(body) { :global(body) {
font-family: 'MinecraftiaRegular'; font-family: 'MinecraftiaRegular';
background-color: #202225; background-color: #202225;
background-image: url("/dirt.jpg"); background-image: url('/dirt.jpg');
} }
</style> </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"> <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 () => { onMount(async () => {
const request = await fetch("/api/v1/me") const request = await fetch('/api/v1/me');
if (request.ok) { if (request.ok) {
loggedIn = true loggedIn = true;
} else { } else {
goto("/") goto('/');
} }
}) });
</script> </script>
{#if !loggedIn} {#if !loggedIn}
<a href="/api/v1/auth/redirect">Login</a> <a href="/api/v1/auth/redirect">Login</a>
{/if} {/if}
<slot></slot> <slot />

View file

@ -1,39 +1,52 @@
<script lang="ts"> <script lang="ts">
import { server } from "$app/env"; import { server } from '$app/env';
import { onMount } from "svelte"; import { onMount } from 'svelte';
let servers = [] let servers = [];
let loaded = false let loaded = false;
onMount(async () => { onMount(async () => {
const request = await fetch("/api/v1/servers") const request = await fetch('/api/v1/servers');
if (request.ok) { if (request.ok) {
loaded = true loaded = true;
servers = await request.json() servers = await request.json();
} else { } 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> </script>
{#if loaded} {#if loaded}
<h2>Your servers <small><a href="/servers/new">new</a></small></h2> <h2>Your servers <small><a href="/servers/new">new</a></small></h2>
{#if servers.length > 0} {#if servers.length > 0}
{#each servers as server} {#each servers as server}
<div> <div>
<h3>{server.name}</h3> <h3>{server.name}</h3>
<p>Server Address: {server.address}</p> <p>Server Address: {server.address}</p>
<p>Server RCON Address: {server.rcon.address}</p> <p>Server RCON Address: {server.rcon.address}</p>
<a href="/servers/{server.id}">new invite</a> <button>delete</button> <a href="/servers/{server.id}">view</a>
</div> <button on:click={deleteServer} data-serverid="{server.id}">delete</button>
{/each} </div>
{/if} {/each}
{/if}
{:else} {:else}
<p>Loading</p> <p>Loading</p>
{/if} {/if}
<style> <style>
div { div {
margin: 0 auto; margin: 0 auto;
} }
</style> </style>

View file

@ -1,57 +1,64 @@
<script lang="ts"> <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) => { let formFields = new FormData(form);
const form: HTMLFormElement = e.currentTarget; let formData = Object.fromEntries(formFields.entries());
const url = form.action; 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); const response = await fetch('/api/v1/servers', {
let formData = Object.fromEntries(formFields.entries()); method: 'POST',
const payload = { body: pl
"address": formData.address, });
"name": formData.name, if (response.ok) {
"rcon": { goto('/servers');
"address": formData.rcon_address, } else {
"password": formData.rcon_password console.log('not ok');
} }
}; };
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")
}
}
</script> </script>
<h2>Add Server</h2> <h2>Add Server</h2>
<form on:submit|preventDefault="{createServer}" action="/api/v1/servers" method="POST"> <form on:submit|preventDefault={createServer} action="/api/v1/servers" method="POST">
<label>Server Name <label
<input type="text" name="name" placeholder="A Minecraft Server"></label> >Server Name
<label>Server Address <input type="text" name="name" placeholder="A Minecraft Server" /></label
<input type="text" name="address" placeholder="127.0.0.1"></label> >
<label>RCON Address <label
<input type="text" name="rcon_address" placeholder="127.0.0.1:25575"></label> >Server Address
<label>RCON Password <input type="text" name="address" placeholder="127.0.0.1" /></label
<input type="password" name="rcon_password" placeholder="rcon password"></label> >
<input type="submit" value="Save"> <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> </form>
<style> <style>
label { label {
display: block; display: block;
margin: 10px 0; margin: 10px 0;
} }
input { input {
border: 1px solid black; border: 1px solid black;
padding: 5px; padding: 5px;
border-radius: 5px; border-radius: 5px;
font-size: large; font-size: large;
} }
</style> </style>