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;
}
footer {
position: absolute;
bottom: 0;
} }
</style> </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,36 +1,35 @@
<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 acceptInvite = async () => {
const response = await fetch(`/api/v1/invite/${code}/accept`, { const response = await fetch(`/api/v1/invite/${code}/accept`, {
method: "POST", method: 'POST'
}); });
if (response.ok) { if (response.ok) {
success = await response.text success = await response.text;
} else { } else {
console.log("not ok") console.log('not ok');
} }
} };
</script> </script>
<svelte:head> <svelte:head>
@ -42,25 +41,25 @@ const acceptInvite = async () => {
</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;
@ -84,20 +83,20 @@ const acceptInvite = async () => {
} }
} }
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';
@ -113,6 +112,6 @@ const acceptInvite = async () => {
: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,35 +1,48 @@
<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>

View file

@ -1,46 +1,53 @@
<script lang="ts"> <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 form: HTMLFormElement = e.currentTarget;
const url = form.action; const url = form.action;
let formFields = new FormData(form); let formFields = new FormData(form);
let formData = Object.fromEntries(formFields.entries()); let formData = Object.fromEntries(formFields.entries());
const payload = { const payload = {
"address": formData.address, address: formData.address,
"name": formData.name, name: formData.name,
"rcon": { rcon: {
"address": formData.rcon_address, address: formData.rcon_address,
"password": formData.rcon_password password: formData.rcon_password
} }
}; };
const pl = JSON.stringify(payload); const pl = JSON.stringify(payload);
const response = await fetch("/api/v1/servers", { const response = await fetch('/api/v1/servers', {
method: "POST", method: 'POST',
body: pl body: pl
}); });
if (response.ok) { if (response.ok) {
goto("/servers") goto('/servers');
} else { } else {
console.log("not ok") 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>