mirror of
https://github.com/1f349/admin.1f349.com.git
synced 2024-11-09 22:32:57 +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_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
|
||||||
|
@ -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
|
||||||
|
68
src/app.scss
68
src/app.scss
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
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 {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
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 {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>
|
||||||
|
@ -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>
|
|
||||||
|
@ -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())
|
||||||
|
Loading…
Reference in New Issue
Block a user