mirror of
https://github.com/1f349/admin.1f349.com.git
synced 2024-11-12 14:41:34 +00:00
New sites view
This commit is contained in:
parent
679080c0ef
commit
8c32bce49e
@ -2,3 +2,4 @@ VITE_SSO_ORIGIN=http://localhost:9090
|
||||
|
||||
VITE_API_VIOLET=http://localhost:9095/v1/violet
|
||||
VITE_API_ORCHID=http://localhost:9095/v1/orchid
|
||||
VITE_API_SITE_HOSTING=http://localhost:9095/v1/sites
|
||||
|
@ -2,3 +2,4 @@ VITE_SSO_ORIGIN=https://sso.1f349.com
|
||||
|
||||
VITE_API_VIOLET=https://api.1f349.com/v1/violet
|
||||
VITE_API_ORCHID=https://api.1f349.com/v1/orchid
|
||||
VITE_API_SITE_HOSTING=https://sites.1f349.com/api.php
|
||||
|
68
src/app.scss
68
src/app.scss
@ -91,3 +91,71 @@ code,
|
||||
.btn-green {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -47,14 +47,6 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
tr:nth-child(2n) {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
tr:nth-child(2n + 1) {
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
tr.created {
|
||||
background-color: #1a5100;
|
||||
|
||||
|
@ -44,14 +44,6 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
tr:nth-child(2n) {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
tr:nth-child(2n + 1) {
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
tr.created {
|
||||
background-color: #1a5100;
|
||||
|
||||
|
13
src/stores/sites.ts
Normal file
13
src/stores/sites.ts
Normal 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}>({});
|
@ -1,15 +1,11 @@
|
||||
import {writable} from "svelte/store";
|
||||
import type {CSPair} from "../types/cspair";
|
||||
import type {Pair} from "../utils/pair";
|
||||
import type {Redirect, Route} from "../types/target";
|
||||
|
||||
export const routesTable = writable<{[key: string]: CSPair<Route>}>({});
|
||||
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>>> {
|
||||
return keys.map(x => ({a: x, b: table[x]}));
|
||||
}
|
||||
|
4
src/utils/pair.ts
Normal file
4
src/utils/pair.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Pair<A, B> {
|
||||
a: A;
|
||||
b: B;
|
||||
}
|
@ -1,21 +1,121 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {domainOption} from "../stores/domain-option";
|
||||
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>
|
||||
|
||||
<div class="sites-panel">
|
||||
<iframe src="https://sites.1f349.com/panel?token={getBearer()}" title="" />
|
||||
<div class="wrapper">
|
||||
<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>
|
||||
|
||||
<style lang="scss">
|
||||
.sites-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.branch-cell {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
}
|
||||
</style>
|
||||
|
@ -118,7 +118,7 @@
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
{:then}
|
||||
<table>
|
||||
<table class="main-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<slot name="headers" />
|
||||
@ -158,55 +158,3 @@
|
||||
{/if}
|
||||
</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>
|
||||
|
@ -150,6 +150,40 @@ func apiServer(verify mjwt.Verifier) {
|
||||
}
|
||||
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) {
|
||||
log.Println("[API Server]", req.URL.String())
|
||||
|
Loading…
Reference in New Issue
Block a user