Start working on domain management tab

This commit is contained in:
Melon 2024-07-19 17:59:14 +01:00
parent f55e6c798a
commit 2f6d043b63
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
9 changed files with 657 additions and 291 deletions

View File

@ -0,0 +1,53 @@
<script lang="ts">
import {isAaaaRecord, isARecord, type AaaaRecord, type ARecord} from "../../stores/records";
import type {RestItem} from "../../utils/rest-table";
import ActionMenu from "../ActionMenu.svelte";
import ActionPopup from "../ActionPopup.svelte";
export let value: RestItem<ARecord | AaaaRecord>;
let editItem: ARecord & AaaaRecord = {
Hdr: {
Name: "",
Rrtype: 0,
Class: 0,
Ttl: 0,
},
A: "",
AAAA: "",
};
let editPopup: boolean = false;
function save() {
value.update(editItem);
}
</script>
<tr>
<td class="code-font">{value.data.Hdr.Name}</td>
<td class="code-font">{isARecord(value.data) ? value.data.A : isAaaaRecord(value.data) ? value.data.AAAA : ""}</td>
<td>
<ActionMenu
data={value}
edit={() => {
editItem = JSON.parse(JSON.stringify(value.data));
editPopup = true;
}}
remove={() => value.remove()}
/>
<ActionPopup name="Edit {isARecord(value.data) ? 'A' : 'AAAA'} Record" bind:show={editPopup} on:save={save}>
<div>Name</div>
<div class="code-font">{editItem.Hdr.Name}</div>
{#if isARecord(value.data)}
<div>IPv4 Address</div>
<div><input type="text" class="code-font" bind:value={editItem.A} size={Math.max(20, editItem.A.length + 2)} /></div>
{:else if isAaaaRecord(value.data)}
<div>IPv6 Address</div>
<div><input type="text" class="code-font" bind:value={editItem.AAAA} size={Math.max(20, editItem.AAAA.length + 2)} /></div>
{:else}
<div>Pretty sure something is broken. WOMP WOMP!!</div>
{/if}
</ActionPopup>
</td>
</tr>

View File

@ -0,0 +1,45 @@
<script lang="ts">
import type {CnameRecord} from "../../stores/records";
import type {RestItem} from "../../utils/rest-table";
import ActionMenu from "../ActionMenu.svelte";
import ActionPopup from "../ActionPopup.svelte";
export let value: RestItem<CnameRecord>;
let editItem: CnameRecord = {
Hdr: {
Name: "",
Rrtype: 0,
Class: 0,
Ttl: 0,
},
Target: "",
};
let editPopup: boolean = false;
function save() {
value.update(editItem);
}
</script>
<tr>
<td class="code-font">{value.data.Hdr.Name}</td>
<td class="code-font">{value.data.Target}</td>
<td>
<ActionMenu
data={value}
edit={() => {
editItem = JSON.parse(JSON.stringify(value.data));
editPopup = true;
}}
remove={() => value.remove()}
/>
<ActionPopup name="Edit CNAME Record" bind:show={editPopup} on:save={save}>
<div>Name</div>
<div class="code-font">{editItem.Hdr.Name}</div>
<div>Target</div>
<div><input type="text" class="code-font" bind:value={editItem.Target} size={Math.max(20, editItem.Target.length + 2)} /></div>
</ActionPopup>
</td>
</tr>

View File

@ -0,0 +1,49 @@
<script lang="ts">
import type {MxRecord} from "../../stores/records";
import type {RestItem} from "../../utils/rest-table";
import ActionMenu from "../ActionMenu.svelte";
import ActionPopup from "../ActionPopup.svelte";
export let value: RestItem<MxRecord>;
let editItem: MxRecord = {
Hdr: {
Name: "",
Rrtype: 0,
Class: 0,
Ttl: 0,
},
Mx: "",
Preference: 0,
};
let editPopup: boolean = false;
function save() {
value.update(editItem);
}
</script>
<tr>
<td class="code-font">{value.data.Hdr.Name}</td>
<td class="code-font">{value.data.Mx}</td>
<td class="code-font">{value.data.Preference}</td>
<td>
<ActionMenu
data={value}
edit={() => {
editItem = JSON.parse(JSON.stringify(value.data));
editPopup = true;
}}
remove={() => value.remove()}
/>
<ActionPopup name="Edit SOA Record" bind:show={editPopup} on:save={save}>
<div>Name</div>
<div class="code-font">{editItem.Hdr.Name}</div>
<div>Mail Server</div>
<div><input type="text" class="code-font" bind:value={editItem.Mx} size={Math.max(20, editItem.Mx.length + 2)} /></div>
<div>Preference</div>
<div><input type="text" class="code-font" bind:value={editItem.Preference} size={Math.max(20, editItem.Preference.length + 2)} /></div>
</ActionPopup>
</td>
</tr>

View File

@ -0,0 +1,45 @@
<script lang="ts">
import type {NsRecord} from "../../stores/records";
import type {RestItem} from "../../utils/rest-table";
import ActionMenu from "../ActionMenu.svelte";
import ActionPopup from "../ActionPopup.svelte";
export let value: RestItem<NsRecord>;
let editItem: NsRecord = {
Hdr: {
Name: "",
Rrtype: 0,
Class: 0,
Ttl: 0,
},
Ns: "",
};
let editPopup: boolean = false;
function save() {
value.update(editItem);
}
</script>
<tr>
<td class="code-font">{value.data.Hdr.Name}</td>
<td class="code-font">{value.data.Ns}</td>
<td>
<ActionMenu
data={value}
edit={() => {
editItem = JSON.parse(JSON.stringify(value.data));
editPopup = true;
}}
remove={() => value.remove()}
/>
<ActionPopup name="Edit SOA Record" bind:show={editPopup} on:save={save}>
<div>Name</div>
<div class="code-font">{editItem.Hdr.Name}</div>
<div>Nameserver</div>
<div><input type="text" class="code-font" bind:value={editItem.Ns} size={Math.max(20, editItem.Ns.length + 2)} /></div>
</ActionPopup>
</td>
</tr>

View File

@ -0,0 +1,63 @@
<script lang="ts">
import type {SoaRecord} from "../../stores/records";
import type {RestItem} from "../../utils/rest-table";
import ActionMenu from "../ActionMenu.svelte";
import ActionPopup from "../ActionPopup.svelte";
export let value: RestItem<SoaRecord>;
let editItem: SoaRecord = {
Hdr: {
Name: "",
Rrtype: 0,
Class: 0,
Ttl: 0,
},
Ns: "",
Mbox: "",
Serial: 0,
Refresh: 0,
Retry: 0,
Expire: 0,
Minttl: 0,
};
let editPopup: boolean = false;
function save() {
value.update(editItem);
}
</script>
<tr>
<td class="code-font">{value.data.Hdr.Name}</td>
<td class="code-font">{value.data.Mbox}</td>
<td class="code-font">{value.data.Minttl}</td>
<td class="code-font">{value.data.Refresh}</td>
<td class="code-font">{value.data.Retry}</td>
<td class="code-font">{value.data.Expire}</td>
<td>
<ActionMenu
data={value}
edit={() => {
editItem = JSON.parse(JSON.stringify(value.data));
editPopup = true;
}}
remove={() => value.remove()}
/>
<ActionPopup name="Edit SOA Record" bind:show={editPopup} on:save={save}>
<div>Name</div>
<div class="code-font">{editItem.Hdr.Name}</div>
<div>Mailbox</div>
<div><input type="text" class="code-font" bind:value={editItem.Mbox} size={Math.max(20, editItem.Mbox.length + 2)} /></div>
<div>Minimum Time-to-Live</div>
<div><input type="number" class="code-font" bind:value={editItem.Minttl} size={Math.max(20, editItem.Minttl.length + 2)} /></div>
<div>Refresh</div>
<div><input type="number" class="code-font" bind:value={editItem.Refresh} size={Math.max(20, editItem.Refresh.length + 2)} /></div>
<div>Retry</div>
<div><input type="number" class="code-font" bind:value={editItem.Retry} size={Math.max(20, editItem.Retry.length + 2)} /></div>
<div>Expire</div>
<div><input type="number" class="code-font" bind:value={editItem.Expire} size={Math.max(20, editItem.Expire.length + 2)} /></div>
</ActionPopup>
</td>
</tr>

View File

@ -0,0 +1,47 @@
<script lang="ts">
import type {TxtRecord} from "../../stores/records";
import type {RestItem} from "../../utils/rest-table";
import ActionMenu from "../ActionMenu.svelte";
import ActionPopup from "../ActionPopup.svelte";
export let value: RestItem<TxtRecord>;
let editItem: TxtRecord = {
Hdr: {
Name: "",
Rrtype: 0,
Class: 0,
Ttl: 0,
},
Txt: [""],
};
let editPopup: boolean = false;
function save() {
value.update(editItem);
}
</script>
<tr>
<td class="code-font">{value.data.Hdr.Name}</td>
<td class="code-font">
<span class="cutoff">{value.data.Txt.join("\n")}</span>
</td>
<td>
<ActionMenu
data={value}
edit={() => {
editItem = JSON.parse(JSON.stringify(value.data));
editPopup = true;
}}
remove={() => value.remove()}
/>
<ActionPopup name="Edit TXT Record" bind:show={editPopup} on:save={save}>
<div>Name</div>
<div class="code-font">{editItem.Hdr.Name}</div>
<div>Value</div>
<div><input type="text" class="code-font" bind:value={editItem.Txt[0]} size={Math.max(20, editItem.Txt[0].length + 2)} /></div>
</ActionPopup>
</td>
</tr>

View File

@ -1,4 +1,12 @@
import {writable} from "svelte/store"; export const DnsTypeSOA = 6;
export const DnsTypeNS = 2;
export const DnsTypeMX = 15;
export const DnsTypeA = 1;
export const DnsTypeAAAA = 28;
export const DnsTypeCNAME = 5;
export const DnsTypeTXT = 16;
export const DnsTypeSRV = 33;
export const DnsTypeCAA = 257;
export interface RecordHeader { export interface RecordHeader {
Name: string; Name: string;
@ -22,72 +30,58 @@ export interface SoaRecord extends UnknownRecord {
} }
export function isSoaRecord(x: UnknownRecord): x is SoaRecord { export function isSoaRecord(x: UnknownRecord): x is SoaRecord {
return x.Hdr.Rrtype === 6; return x.Hdr.Rrtype === DnsTypeSOA;
} }
export const soaRecords = writable<Array<SoaRecord>>([]);
export interface NsRecord extends UnknownRecord { export interface NsRecord extends UnknownRecord {
Ns: string; Ns: string;
} }
export function isNsRecord(x: UnknownRecord): x is NsRecord { export function isNsRecord(x: UnknownRecord): x is NsRecord {
return x.Hdr.Rrtype === 2; return x.Hdr.Rrtype === DnsTypeNS;
} }
export const nsRecords = writable<Array<NsRecord>>([]);
export interface MxRecord extends UnknownRecord { export interface MxRecord extends UnknownRecord {
Preference: number; Preference: number;
Mx: string; Mx: string;
} }
export function isMxRecord(x: UnknownRecord): x is MxRecord { export function isMxRecord(x: UnknownRecord): x is MxRecord {
return x.Hdr.Rrtype === 15; return x.Hdr.Rrtype === DnsTypeMX;
} }
export const mxRecords = writable<Array<MxRecord>>([]);
export interface ARecord extends UnknownRecord { export interface ARecord extends UnknownRecord {
A: string; A: string;
} }
export function isARecord(x: UnknownRecord): x is ARecord { export function isARecord(x: UnknownRecord): x is ARecord {
return x.Hdr.Rrtype === 1; return x.Hdr.Rrtype === DnsTypeA;
} }
export const aRecords = writable<Array<ARecord>>([]);
export interface AaaaRecord extends UnknownRecord { export interface AaaaRecord extends UnknownRecord {
AAAA: string; AAAA: string;
} }
export function isAaaaRecord(x: UnknownRecord): x is AaaaRecord { export function isAaaaRecord(x: UnknownRecord): x is AaaaRecord {
return x.Hdr.Rrtype === 28; return x.Hdr.Rrtype === DnsTypeAAAA;
} }
export const aaaaRecords = writable<Array<AaaaRecord>>([]);
export interface CnameRecord extends UnknownRecord { export interface CnameRecord extends UnknownRecord {
Target: string; Target: string;
} }
export function isCnameRecord(x: UnknownRecord): x is CnameRecord { export function isCnameRecord(x: UnknownRecord): x is CnameRecord {
return x.Hdr.Rrtype === 5; return x.Hdr.Rrtype === DnsTypeCNAME;
} }
export const cnameRecords = writable<Array<CnameRecord>>([]);
export interface TxtRecord extends UnknownRecord { export interface TxtRecord extends UnknownRecord {
Txt: Array<string>; Txt: Array<string>;
} }
export function isTxtRecord(x: UnknownRecord): x is TxtRecord { export function isTxtRecord(x: UnknownRecord): x is TxtRecord {
return x.Hdr.Rrtype === 16; return x.Hdr.Rrtype === DnsTypeTXT;
} }
export const txtRecords = writable<Array<TxtRecord>>([]);
export interface SrvRecord extends UnknownRecord { export interface SrvRecord extends UnknownRecord {
Priority: number; Priority: number;
Weight: number; Weight: number;
@ -96,11 +90,9 @@ export interface SrvRecord extends UnknownRecord {
} }
export function isSrvRecord(x: UnknownRecord): x is SrvRecord { export function isSrvRecord(x: UnknownRecord): x is SrvRecord {
return x.Hdr.Rrtype === 33; return x.Hdr.Rrtype === DnsTypeSRV;
} }
export const srvRecords = writable<Array<SrvRecord>>([]);
export interface CaaRecord extends UnknownRecord { export interface CaaRecord extends UnknownRecord {
Flag: number; Flag: number;
Tag: string; Tag: string;
@ -108,7 +100,5 @@ export interface CaaRecord extends UnknownRecord {
} }
export function isCaaRecord(x: UnknownRecord): x is CaaRecord { export function isCaaRecord(x: UnknownRecord): x is CaaRecord {
return x.Hdr.Rrtype === 257; return x.Hdr.Rrtype === DnsTypeCAA;
} }
export const caaRecords = writable<Array<CaaRecord>>([]);

View File

@ -2,10 +2,7 @@
import {LOGIN} from "../utils/login"; import {LOGIN} from "../utils/login";
import {domainOption} from "../stores/domain-option"; import {domainOption} from "../stores/domain-option";
import { import {
aRecords, DnsTypeSOA,
aaaaRecords,
caaRecords,
cnameRecords,
isARecord, isARecord,
isAaaaRecord, isAaaaRecord,
isCaaRecord, isCaaRecord,
@ -15,38 +12,50 @@
isSoaRecord, isSoaRecord,
isSrvRecord, isSrvRecord,
isTxtRecord, isTxtRecord,
mxRecords, type ARecord,
nsRecords, type AaaaRecord,
soaRecords, type CaaRecord,
srvRecords, type CnameRecord,
txtRecords, type MxRecord,
type NsRecord,
type SoaRecord,
type SrvRecord,
type TxtRecord,
type UnknownRecord, type UnknownRecord,
} from "../stores/records"; } from "../stores/records";
import ActionMenu from "../components/ActionMenu.svelte"; import ActionMenu from "../components/ActionMenu.svelte";
import PromiseTable from "../components/PromiseTable.svelte";
import {RestItem, RestTable} from "../utils/rest-table";
import PromiseLike from "../components/PromiseLike.svelte";
import SoaRow from "../components/domains/SoaRow.svelte";
const apiAzalea = import.meta.env.VITE_API_AZALEA; const apiAzalea = import.meta.env.VITE_API_AZALEA;
let promiseForTable: Promise<void> = reloadTable(); type AllRecords = SoaRecord | NsRecord | MxRecord | ARecord | AaaaRecord | CnameRecord | TxtRecord | SrvRecord | CaaRecord;
async function reloadTable(): Promise<void> { const table = new RestTable<AllRecords>(apiAzalea + "/domains/" + $domainOption + "/records", (item: AllRecords) => item.Hdr.Name);
let f = await LOGIN.clientRequest(apiAzalea + "/domains/" + $domainOption + "/records", {});
if (f.status != 200) throw new Error("Unexpected status code: " + f.status); function rowOrdering<T extends UnknownRecord>(
let fJson = await f.json(); rows: RestItem<UnknownRecord>[],
let rows = fJson as Array<UnknownRecord>; domain: string,
$soaRecords = rows.filter(isSoaRecord); isTRecord: (t: UnknownRecord) => t is T,
$nsRecords = rows.filter(isNsRecord); ): RestItem<T>[] {
$mxRecords = rows.filter(isMxRecord); return rows
$aRecords = rows.filter(isARecord); .filter(x => isTRecord(x.data))
$aaaaRecords = rows.filter(isAaaaRecord); .filter(x => domainFilter(x.data.Hdr.Name, domain))
$cnameRecords = rows.filter(isCnameRecord); .sort((a, b) => a.data.Hdr.Name.localeCompare(b.data.Hdr.Name)) as unknown as RestItem<T>[];
$txtRecords = rows.filter(isTxtRecord);
$srvRecords = rows.filter(isSrvRecord);
$caaRecords = rows.filter(isCaaRecord);
} }
domainOption.subscribe(x => { function domainFilter(src: string, domain: string) {
promiseForTable = reloadTable(); 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);
}
domainOption.subscribe(() => table.reload());
function getTitleDomain(name: string): string { function getTitleDomain(name: string): string {
if (name.endsWith(".")) { if (name.endsWith(".")) {
@ -54,16 +63,29 @@
} }
return name; return name;
} }
let soaRecords: SoaRecord[] = [
{
Hdr: {
Name: "example.com.",
Rrtype: DnsTypeSOA,
Class: 1,
Ttl: 300,
},
Ns: "ns1.example.com.",
Mbox: "postmaster.example.com.",
Serial: 0,
Refresh: 0,
Retry: 0,
Expire: 0,
Minttl: 0,
},
];
</script> </script>
{#await promiseForTable} {#if soaRecords.length >= 1}
<div class="text-padding">
<div>Loading...</div>
</div>
{:then}
{#if $soaRecords.length >= 1}
<div class="title-row"> <div class="title-row">
<h1>Domains / {getTitleDomain($soaRecords[0].Hdr.Name)}</h1> <h1>Domains / {getTitleDomain(soaRecords[0].Hdr.Name)}</h1>
<a <a
class="zone-download" class="zone-download"
href="{import.meta.env.VITE_API_AZALEA}/domains/{getTitleDomain($soaRecords[0].Hdr.Name)}/zone-file" href="{import.meta.env.VITE_API_AZALEA}/domains/{getTitleDomain($soaRecords[0].Hdr.Name)}/zone-file"
@ -75,6 +97,35 @@
{/if} {/if}
<h2>SOA Record</h2> <h2>SOA Record</h2>
<PromiseTable value={table}>
<tr slot="headers">
<th>Primary Domain</th>
<th>Email</th>
<th>Default TTL</th>
<th>Refresh Rate</th>
<th>Retry Rate</th>
<th>Expire Time</th>
<th></th>
</tr>
<svelte:fragment slot="rows" let:value>
{#each rowOrdering(value.rows, $domainOption, DnsTypeSOA) as item}
<PromiseLike value={item}>
<tr slot="loading" class="empty-row">
<td colspan="100">
<div>Loading...</div>
</td>
</tr>
<tr slot="error" let:reason class="empty-row">
<td colspan="100">Error loading row for {item.data.Hdr.Name}: {reason}</td>
</tr>
<SoaRow slot="ok" let:value {value} />
</PromiseLike>
{/each}
</svelte:fragment>
</PromiseTable>
<table class="action-table" aria-label="List of Domains SOA Record"> <table class="action-table" aria-label="List of Domains SOA Record">
<thead> <thead>
<tr> <tr>
@ -242,7 +293,9 @@
{#each $txtRecords as record} {#each $txtRecords as record}
<tr> <tr>
<td>{record.Hdr.Name}</td> <td>{record.Hdr.Name}</td>
<td>{record.Txt.join("\n")}</td> <td>
<span class="cutoff">{record.Txt.join("\n")}</span>
</td>
<td>{record.Hdr.Ttl}</td> <td>{record.Hdr.Ttl}</td>
<td> <td>
<ActionMenu data={record} edit={t => console.log(t)} remove={t => console.log(t)} /> <ActionMenu data={record} edit={t => console.log(t)} remove={t => console.log(t)} />
@ -313,7 +366,6 @@
{/each} {/each}
</tbody> </tbody>
</table> </table>
{/await}
<style lang="scss"> <style lang="scss">
@import "../values.scss"; @import "../values.scss";
@ -322,9 +374,30 @@
@include button-green-highlight; @include button-green-highlight;
} }
table tbody tr.empty-row td { table tbody tr {
td {
position: relative;
span.cutoff {
position: absolute;
top: 50%;
left: 0;
right: 0;
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
margin-inline: 15px;
display: inline-block;
vertical-align: middle;
line-height: 1rem;
transform: translateY(-50%);
}
}
&.empty-row td {
text-align: center; text-align: center;
} }
}
.title-row { .title-row {
display: flex; display: flex;

View File

@ -52,6 +52,7 @@ func ssoServer(signer mjwt.Signer) {
ps := claims.NewPermStorage() ps := claims.NewPermStorage()
ps.Set("violet:route") ps.Set("violet:route")
ps.Set("violet:redirect") ps.Set("violet:redirect")
ps.Set("azalea:domains")
ps.Set("domain:owns=example.com") ps.Set("domain:owns=example.com")
ps.Set("domain:owns=example.org") ps.Set("domain:owns=example.org")
accessToken, err := signer.GenerateJwt("81b99bd7-bf74-4cc2-9133-80ed2393dfe6", uuid.NewString(), jwt.ClaimStrings{"b5a9a8df-827c-4925-b1c1-1940abcf356b"}, 15*time.Minute, auth.AccessTokenClaims{ accessToken, err := signer.GenerateJwt("81b99bd7-bf74-4cc2-9133-80ed2393dfe6", uuid.NewString(), jwt.ClaimStrings{"b5a9a8df-827c-4925-b1c1-1940abcf356b"}, 15*time.Minute, auth.AccessTokenClaims{