New sites view

This commit is contained in:
Melon 2023-11-13 16:44:12 +00:00
parent 679080c0ef
commit 8c32bce49e
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
11 changed files with 236 additions and 87 deletions

View File

@ -2,3 +2,4 @@ VITE_SSO_ORIGIN=http://localhost:9090
VITE_API_VIOLET=http://localhost:9095/v1/violet VITE_API_VIOLET=http://localhost:9095/v1/violet
VITE_API_ORCHID=http://localhost:9095/v1/orchid VITE_API_ORCHID=http://localhost:9095/v1/orchid
VITE_API_SITE_HOSTING=http://localhost:9095/v1/sites

View File

@ -2,3 +2,4 @@ VITE_SSO_ORIGIN=https://sso.1f349.com
VITE_API_VIOLET=https://api.1f349.com/v1/violet VITE_API_VIOLET=https://api.1f349.com/v1/violet
VITE_API_ORCHID=https://api.1f349.com/v1/orchid VITE_API_ORCHID=https://api.1f349.com/v1/orchid
VITE_API_SITE_HOSTING=https://sites.1f349.com/api.php

View File

@ -91,3 +91,71 @@ code,
.btn-green { .btn-green {
background-color: #04aa6d; background-color: #04aa6d;
} }
table.main-table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
thead {
background-color: #333333;
position: sticky;
top: 0;
z-index: 9999;
box-shadow: 0 4px 8px #0003, 0 6px 20px #00000030;
}
th,
td {
padding: 6px 8px 6px 8px;
text-align: center;
}
tr:nth-child(2n) {
background-color: #2a2a2a;
}
tr:nth-child(2n + 1) {
background-color: #242424;
}
.invert-rows {
tr:nth-child(2n) {
background-color: #242424;
}
tr:nth-child(2n + 1) {
background-color: #2a2a2a;
}
}
}
.wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
.scrolling-area {
overflow: auto;
height: 100%;
}
}
.text-padding {
padding: 4px 16px;
}
.footer {
height: 50px;
background-color: #2c2c2c;
box-shadow: 0 -4px 8px #0003, 0 -6px 20px #00000030;
display: flex;
flex-direction: row;
.meta-info {
line-height: 50px;
padding-inline: 16px;
}
}

View File

@ -47,14 +47,6 @@
{/if} {/if}
<style lang="scss"> <style lang="scss">
tr:nth-child(2n) {
background-color: #2a2a2a;
}
tr:nth-child(2n + 1) {
background-color: #242424;
}
tr.created { tr.created {
background-color: #1a5100; background-color: #1a5100;

View File

@ -44,14 +44,6 @@
{/if} {/if}
<style lang="scss"> <style lang="scss">
tr:nth-child(2n) {
background-color: #2a2a2a;
}
tr:nth-child(2n + 1) {
background-color: #242424;
}
tr.created { tr.created {
background-color: #1a5100; background-color: #1a5100;

13
src/stores/sites.ts Normal file
View File

@ -0,0 +1,13 @@
import {writable} from "svelte/store";
export interface Site {
domain: string;
branches: string[];
}
export function siteEqual(a: Site | null, b: Site | null) {
if (a == null || b == null) return false;
return a.domain == b.domain;
}
export const sitesTable = writable<{[key: string]: Site}>({});

View File

@ -1,15 +1,11 @@
import {writable} from "svelte/store"; import {writable} from "svelte/store";
import type {CSPair} from "../types/cspair"; import type {CSPair} from "../types/cspair";
import type {Pair} from "../utils/pair";
import type {Redirect, Route} from "../types/target"; import type {Redirect, Route} from "../types/target";
export const routesTable = writable<{[key: string]: CSPair<Route>}>({}); export const routesTable = writable<{[key: string]: CSPair<Route>}>({});
export const redirectsTable = writable<{[key: string]: CSPair<Redirect>}>({}); export const redirectsTable = writable<{[key: string]: CSPair<Redirect>}>({});
export interface Pair<A, B> {
a: A;
b: B;
}
function getTableArray<T>(table: {[key: string]: CSPair<T>}, keys: Array<string>): Array<Pair<string, CSPair<T>>> { function getTableArray<T>(table: {[key: string]: CSPair<T>}, keys: Array<string>): Array<Pair<string, CSPair<T>>> {
return keys.map(x => ({a: x, b: table[x]})); return keys.map(x => ({a: x, b: table[x]}));
} }

4
src/utils/pair.ts Normal file
View File

@ -0,0 +1,4 @@
export interface Pair<A, B> {
a: A;
b: B;
}

View File

@ -1,21 +1,121 @@
<script> <script lang="ts">
import {domainOption} from "../stores/domain-option";
import {getBearer} from "../stores/login"; import {getBearer} from "../stores/login";
import {type Site, sitesTable} from "../stores/sites";
const apiSiteHosting = import.meta.env.VITE_API_SITE_HOSTING;
let tableKeys: string[] = [];
$: tableKeys = Object.entries($sitesTable)
.map(x => x[0])
.filter(x => domainFilter(x, $domainOption))
.sort((a, b) => a.localeCompare(b));
function domainFilter(src: string, domain: string) {
if (domain == "*") return true;
let n = src.indexOf("/");
if (n == -1) n = src.length;
let p = src.slice(0, n);
if (p == domain) return true;
return p.endsWith(domain);
}
let promiseForTable: Promise<void> = Object.entries($sitesTable).length === 0 ? reloadTable() : Promise.resolve();
function reloadTable(): Promise<void> {
return new Promise<void>((res, rej) => {
fetch(apiSiteHosting, {headers: {Authorization: getBearer()}})
.then(x => {
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
return x.json();
})
.then(x => {
let rows = x as Site[];
rows.forEach(x => {
$sitesTable[x.domain] = x;
});
res();
})
.catch(x => rej(x));
});
}
function deleteBranch(site: Site, branch: string) {
fetch(apiSiteHosting, {
method: "POST",
headers: {Authorization: getBearer()},
body: JSON.stringify({submit: "delete-branch", site: site.domain, branch}),
})
.then(x => {
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
promiseForTable = reloadTable();
})
.catch(x => alert("Error deleting branch: " + x));
}
function resetSiteSecret(site: Site) {
fetch(apiSiteHosting, {
method: "POST",
headers: {Authorization: getBearer()},
body: JSON.stringify({submit: "secret", site: site.domain}),
})
.then(x => {
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
return x.json();
})
.then(x => {
alert("New secret: " + x.secret);
})
.catch(x => alert("Error resetting secret: " + x));
}
</script> </script>
<div class="sites-panel"> <div class="wrapper">
<iframe src="https://sites.1f349.com/panel?token={getBearer()}" title="" /> <div style="padding:8px;background-color:#bb7900;">
Warning: This is currently still under development, however it DOES send updates to the real server
</div>
<div class="scrolling-area">
{#await promiseForTable}
<div class="text-padding">
<div>Loading...</div>
</div>
{:then}
<table class="main-table">
<thead>
<tr>
<th>Domain</th>
<th>Branches</th>
<th>Action</th>
</tr>
</thead>
<tbody class="invert-rows">
{#each tableKeys as key (key)}
<tr>
<td><a href="https://{$sitesTable[key].domain}" target="_blank">{$sitesTable[key].domain}</a></td>
<td class="branch-cell">
{#each $sitesTable[key].branches as branch}
<div>{branch}</div>
<div><button on:click={() => deleteBranch($sitesTable[key], branch)}>Delete Branch</button></div>
{/each}
</td>
<td><button on:click={() => resetSiteSecret($sitesTable[key])}>Reset Secret</button></td>
</tr>
{/each}
</tbody>
</table>
{:catch err}
<div class="text-padding">
<div>Administrator... I hardly know her?</div>
<div>{err}</div>
</div>
{/await}
</div>
</div> </div>
<style lang="scss"> <style lang="scss">
.sites-panel { .branch-cell {
width: 100%; display: grid;
height: 100%; grid-template-columns: repeat(2, auto);
overflow: hidden;
iframe {
border: 0;
width: 100%;
height: 100%;
}
} }
</style> </style>

View File

@ -118,7 +118,7 @@
<div>Loading...</div> <div>Loading...</div>
</div> </div>
{:then} {:then}
<table> <table class="main-table">
<thead> <thead>
<tr> <tr>
<slot name="headers" /> <slot name="headers" />
@ -158,55 +158,3 @@
{/if} {/if}
</div> </div>
</div> </div>
<style lang="scss">
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
thead {
background-color: #333333;
position: sticky;
top: 0;
z-index: 9999;
box-shadow: 0 4px 8px #0003, 0 6px 20px #00000030;
}
:global(th),
:global(td) {
padding: 6px 8px 6px 8px;
text-align: center;
}
}
.wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
.scrolling-area {
overflow: auto;
height: 100%;
}
}
.text-padding {
padding: 4px 16px;
}
.footer {
height: 50px;
background-color: #2c2c2c;
box-shadow: 0 -4px 8px #0003, 0 -6px 20px #00000030;
display: flex;
flex-direction: row;
.meta-info {
line-height: 50px;
padding-inline: 16px;
}
}
</style>

View File

@ -150,6 +150,40 @@ func apiServer(verify mjwt.Verifier) {
} }
json.NewEncoder(rw).Encode(m) json.NewEncoder(rw).Encode(m)
})) }))
r.Handle("/v1/sites", hasPerm(verify, "sites:manage", func(rw http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodPost {
defer req.Body.Close()
dec := json.NewDecoder(req.Body)
var m map[string]string
if err := dec.Decode(&m); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
switch m["submit"] {
case "secret":
rw.WriteHeader(http.StatusOK)
fmt.Fprintf(rw, "{\"secret\":\"%s\"}\n", uuid.NewString())
return
case "delete-branch":
rw.WriteHeader(http.StatusOK)
}
return
}
m := make([]any, 0, 40)
for i := 0; i < 20; i++ {
m = append(m, map[string]any{
"domain": uuid.NewString() + ".example.com",
"branches": []string{"@", "@beta"},
})
}
for i := 0; i < 20; i++ {
m = append(m, map[string]any{
"domain": uuid.NewString() + ".example.org",
"branches": []string{"@", "@alpha"},
})
}
json.NewEncoder(rw).Encode(m)
}))
logger := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { logger := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
log.Println("[API Server]", req.URL.String()) log.Println("[API Server]", req.URL.String())