init commit
This commit is contained in:
commit
dfe324cf8f
43 changed files with 4237 additions and 0 deletions
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
||||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss"
|
||||||
|
},
|
||||||
|
"piny.project_settings": {
|
||||||
|
"open-pages": [
|
||||||
|
"pinyfakepage.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
41
README.md
Normal file
41
README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by
|
||||||
|
[`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# create a new project in the current directory
|
||||||
|
npx sv create
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or
|
||||||
|
`pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an
|
||||||
|
> [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
41
eslint.config.js
Normal file
41
eslint.config.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { includeIgnoreFile } from "@eslint/compat";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import svelte from "eslint-plugin-svelte";
|
||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
import globals from "globals";
|
||||||
|
import ts from "typescript-eslint";
|
||||||
|
import svelteConfig from "./svelte.config.js";
|
||||||
|
|
||||||
|
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
includeIgnoreFile(gitignorePath),
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser, ...globals.node },
|
||||||
|
},
|
||||||
|
rules: { // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||||
|
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||||
|
"no-undef": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"**/*.svelte",
|
||||||
|
"**/*.svelte.ts",
|
||||||
|
"**/*.svelte.js",
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
extraFileExtensions: [".svelte"],
|
||||||
|
parser: ts.parser,
|
||||||
|
svelteConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
40
package.json
Normal file
40
package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "dns-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"daisyui": "^5.5.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.4.0",
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@sveltejs/adapter-node": "^5.4.0",
|
||||||
|
"@sveltejs/kit": "^2.48.5",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
|
"@types/node": "^22",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-svelte": "^3.13.0",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"prettier": "^3.7.4",
|
||||||
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
|
"svelte": "^5.43.8",
|
||||||
|
"svelte-check": "^4.3.4",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.47.0",
|
||||||
|
"vite": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
11
src/app.html
Normal file
11
src/app.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
src/lib/CollapsibleSubmenu.svelte
Normal file
0
src/lib/CollapsibleSubmenu.svelte
Normal file
33
src/lib/ErrorPopup.svelte
Normal file
33
src/lib/ErrorPopup.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { fade, fly } from "svelte/transition";
|
||||||
|
|
||||||
|
let { errorMessage = null as string | null, duration = 5000 } = $props();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!errorMessage) {
|
||||||
|
visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visible = true;
|
||||||
|
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
visible = false;
|
||||||
|
timeoutId = null;
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
let visible: boolean = $state(false);
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div role="alert" class="alert alert-error font-bold mt-5 z-0"
|
||||||
|
in:fly={{ y: -70, duration: 500 }}
|
||||||
|
out:fade={{ duration: 300 }}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
|
<span>{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
87
src/lib/MainNavbar.svelte
Normal file
87
src/lib/MainNavbar.svelte
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import { auth, setUserLoggedOut } from "./auth.svelte";
|
||||||
|
|
||||||
|
async function toggleMfa() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/user/update-mfa`, {
|
||||||
|
credentials: "include",
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ enable_mfa: auth.isMfaEnabled }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 401 || res.status === 500) {
|
||||||
|
setUserLoggedOut();
|
||||||
|
goto('/login');
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
} else if (res.status !== 204) {
|
||||||
|
let msg = await res.text();
|
||||||
|
throw new Error(`Failed updating MFA: ${msg}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: show error to user
|
||||||
|
console.error(err);
|
||||||
|
auth.isMfaEnabled = !auth.isMfaEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="navbar bg-base-100 shadow-sm top-0 z-10 py-0">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<div class="dropdown">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /> </svg>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="-1"
|
||||||
|
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/about">About us</a></li>
|
||||||
|
<li><a href="/faq">FAQ</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-ghost text-xl" href="/">HexName</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-center hidden lg:flex">
|
||||||
|
<ul class="menu menu-horizontal px-1">
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/about">About us</a></li>
|
||||||
|
<li><a href="/faq">FAQ</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-end">
|
||||||
|
{#if auth.isAuthenticated}
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
|
||||||
|
<svg class="size-9" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#dfe5ed"><path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="-1"
|
||||||
|
class="menu menu-sm dropdown-content bg-base-200 rounded-box z-10 mt-3 w-52 p-2 shadow">
|
||||||
|
<li><text class="font-bold">{auth.userEmail}</text></li>
|
||||||
|
<li>
|
||||||
|
<div class="justify-between">
|
||||||
|
2-factor via email
|
||||||
|
<label class="toggle text-base-content outline-transparent">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={auth.isMfaEnabled}
|
||||||
|
on:change={toggleMfa}
|
||||||
|
/>
|
||||||
|
<svg class="outline-transparent" aria-label="disabled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12"/></svg>
|
||||||
|
<svg class="outline-transparent" aria-label="enabled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g stroke-linejoin="round" stroke-linecap="round" stroke-width="4" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"></path></g></svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li><a href="/delete-account">Delete account</a></li>
|
||||||
|
<li><a href="/logout">Log out</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<a href="/register"><button class="btn btn-sm btn-primary rounded-lg mr-2">Create an account</button></a>
|
||||||
|
<a href="/login"><button class="btn btn-sm btn-secondary rounded-lg mr-2">Log in</button></a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
261
src/lib/Record.svelte
Normal file
261
src/lib/Record.svelte
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from "$app/state";
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import ErrorPopup from "./ErrorPopup.svelte";
|
||||||
|
import RecordExplanation from "./RecordExplanation.svelte";
|
||||||
|
import { getDisplayName, getDisplayValue, getNamePlaceholderText, getValuePlaceholderText, getValueTitleText, handleUiRecordUpdates, isRecordValid, type DnsRecord } from "./records.svelte";
|
||||||
|
|
||||||
|
let { records = $bindable(), record, i, fqdn, error }: { records: DnsRecord[], record: DnsRecord, i: number, fqdn: string, error: string } = $props();
|
||||||
|
|
||||||
|
let isEdit = $state(false);
|
||||||
|
let recordDraft = $state($state.snapshot(record));
|
||||||
|
let errorMessage = $state('');
|
||||||
|
|
||||||
|
function triggerMenu() {
|
||||||
|
if (isEdit) {
|
||||||
|
isEdit = false;
|
||||||
|
} else {
|
||||||
|
recordDraft = $state.snapshot(record);
|
||||||
|
recordDraft.name = recordDraft.name ? recordDraft.name : '@';
|
||||||
|
isEdit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdateSubmit(e: SubmitEvent) {
|
||||||
|
errorMessage = isRecordValid(record.record_type, recordDraft.name, recordDraft.value, recordDraft.priority ?? 0, recordDraft.weight ?? 0, recordDraft.port ?? 0, recordDraft.comment, recordDraft.ttl);
|
||||||
|
if (errorMessage !== '') {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let processedName = recordDraft.name === '@' ? '' : recordDraft.name
|
||||||
|
|
||||||
|
try {
|
||||||
|
let res;
|
||||||
|
|
||||||
|
if (record.record_type === "MX") {
|
||||||
|
let processedValue = recordDraft.value === '@' ? fqdn : recordDraft.value;
|
||||||
|
res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"record_id": record.id, "is_active": recordDraft.is_active, "name": processedName, "value": processedValue, "ttl": recordDraft.ttl, "comment": recordDraft.comment, "record_type": record.record_type, "subdomain_id": page.params.id, "priority": recordDraft.priority})
|
||||||
|
});
|
||||||
|
} else if (record.record_type === "SRV") {
|
||||||
|
let processedValue = recordDraft.value === '@' ? fqdn : recordDraft.value;
|
||||||
|
res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"record_id": record.id, "is_active": recordDraft.is_active, "name": processedName, "value": processedValue, "ttl": recordDraft.ttl, "comment": recordDraft.comment, "record_type": record.record_type, "subdomain_id": page.params.id, "priority": recordDraft.priority, "weight": recordDraft.weight, "port": recordDraft.port})
|
||||||
|
});
|
||||||
|
} else if (record.record_type === "NS" || record.record_type === "CNAME") {
|
||||||
|
let processedValue = recordDraft.value === '@' ? fqdn : recordDraft.value;
|
||||||
|
res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"record_id": record.id, "is_active": recordDraft.is_active, "name": processedName, "value": processedValue, "ttl": recordDraft.ttl, "comment": recordDraft.comment, "record_type": record.record_type, "subdomain_id": page.params.id })
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"record_id": record.id, "is_active": recordDraft.is_active, "name": processedName, "value": recordDraft.value, "ttl": recordDraft.ttl, "comment": recordDraft.comment, "record_type": record.record_type, "subdomain_id": page.params.id })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
console.log(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
records = handleUiRecordUpdates(records, data);
|
||||||
|
alert("Record successfully updated!")
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
console.log(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteRecord(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement; }) {
|
||||||
|
try {
|
||||||
|
let res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"record_id": record.id })});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = (await res.json()).msg || "Something went wrong";
|
||||||
|
console.log(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
records.splice(i, 1);
|
||||||
|
isEdit = false;
|
||||||
|
alert("Record successfully deleted");
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
console.log(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tr
|
||||||
|
class={isEdit
|
||||||
|
? "bg-base-300"
|
||||||
|
:
|
||||||
|
`${
|
||||||
|
i % 2 === 0 ? '[&>td]:bg-base-100' : '[&>td]:bg-base-200'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<td class="max-w-25 w-25">
|
||||||
|
<span class="badge badge-outline px-2">{record.record_type}</span>
|
||||||
|
</td>
|
||||||
|
<td class="font-mono truncate">{record.name || '@'}</td>
|
||||||
|
<td class="max-w-40 font-mono truncate">{record.value}</td>
|
||||||
|
<td class="w-10 truncate hidden lg:table-cell">{record.ttl}</td>
|
||||||
|
<td class="max-w-40 truncate hidden lg:table-cell">{record.comment || '-'}</td>
|
||||||
|
<td class="w-0 pr-1 hidden lg:table-cell">
|
||||||
|
{#if record.is_active}
|
||||||
|
<span class="badge badge-success p-2 w-16">Active</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-error p-2 w-16">Inactive</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="w-0 p-0 px-4">
|
||||||
|
{#if isEdit}
|
||||||
|
<button class="btn btn-xs tooltip rounded-md p-0 w-7 h-7" data-tip="Discard changes" onclick={triggerMenu}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn btn-xs tooltip rounded-md p-0 w-7 h-7" data-tip="Edit" onclick={triggerMenu}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{#if isEdit}
|
||||||
|
<tr class="bg-base-200">
|
||||||
|
<td colspan="99" class="rounded">
|
||||||
|
<form onsubmit={handleUpdateSubmit}>
|
||||||
|
<div class="flex flex-col sm:flex-row justify-center">
|
||||||
|
<div class="m-2 flex-1">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<label class="fieldset-legend" for="record-name">Name</label>
|
||||||
|
<input class="input validator w-full" id="record-name" bind:value={recordDraft.name} placeholder={getNamePlaceholderText(record.record_type)} required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if record.record_type === 'MX'}
|
||||||
|
<div class="m-2 sm:w-20">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="fieldset-legend mr-1" for="record-priority">Priority</label>
|
||||||
|
<div class="tooltip tooltip-info mt-1" data-tip="This field is useful if you have multiple mailservers. The server with the lower priority is tried first. If it fails to receive mail, the one with the next lowest priority is tried. Set the same priority values for for load-balancing.">
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column justify-center ml-auto ">
|
||||||
|
<svg class="stroke-primary-content h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="record-priority" type="number" class="input input-bordered" bind:value={recordDraft.priority} placeholder="10" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if record.record_type === 'SRV'}
|
||||||
|
<div class="m-2 sm:w-20">
|
||||||
|
<label class="form-control">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="fieldset-legend mr-1" for="record-priority">Priority</label>
|
||||||
|
<div class="tooltip tooltip-info mt-1" data-tip="This field is useful if you have multiple servers. The server with the lower priority is prioritized for traffic.">
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column justify-center ml-auto ">
|
||||||
|
<svg class="stroke-primary-content h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="record-priority" type="number" class="input validator" bind:value={recordDraft.priority} placeholder="10" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-2 sm:w-20">
|
||||||
|
<label class="form-control">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="fieldset-legend mr-1" for="record-weight">Weight</label>
|
||||||
|
<div class="tooltip tooltip-info mt-1" data-tip="This field serves a similar purpose as the Priority, except it is looked at second. So priority takes precedence.">
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column justify-center ml-auto ">
|
||||||
|
<svg class="stroke-primary-content h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="record-weight" type="number" class="input validator" bind:value={recordDraft.weight} placeholder="5" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-2 sm:w-20">
|
||||||
|
<label class="form-control">
|
||||||
|
<label class="fieldset-legend" for="record-port">Port</label>
|
||||||
|
<input id="record-port" type="number" class="input validator" bind:value={recordDraft.port} placeholder="8080" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="m-2 flex-1">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<label class="fieldset-legend" for="record-value">{getValueTitleText(record.record_type)}</label>
|
||||||
|
<input class="input validator w-full" id="record-value" bind:value={recordDraft.value} placeholder={getValuePlaceholderText(record.record_type)} required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col sm:flex-row">
|
||||||
|
<label class="m-2 form-control flex flex-1 flex-col">
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<label class="fieldset-legend" for="record-comment">Comment</label>
|
||||||
|
<div class="py-2 -mb-1 pl-1 text-primary-content/50">(optional)</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-center items-center">
|
||||||
|
<input class="input validator w-full mr-auto" id="record-comment" bind:value={recordDraft.comment} placeholder="This is my favorite DNS record!"/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="m-2 w-fill sm:w-30">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="fieldset-legend mr-1" for="record-ttl">TTL</label>
|
||||||
|
<div class="tooltip tooltip-info mt-1" data-tip="TTL or 'Time To Live' controls how long the record is allowed to be cached on different devices. Set a lower number if you expect your record to change value very often.">
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column justify-center ml-auto ">
|
||||||
|
<svg class="stroke-primary-content h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="number" class="input input-bordered" id="record-ttl" bind:value={recordDraft.ttl} placeholder="300" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row mx-2 sm:m-0 justify-center items-center mb-0">
|
||||||
|
<label class="label my-2 sm:mr-auto sm:ml-2">
|
||||||
|
Activate / deactivate record
|
||||||
|
<input
|
||||||
|
class="toggle toggle-xl sm:toggle-md border-error text-error-text bg-error checked:border-success checked:bg-success checked:text-success-text"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={recordDraft.is_active}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-error m-2 p-0 w-full sm:w-30" type="button" onclick={handleDeleteRecord}>Delete record</button>
|
||||||
|
<button class="btn btn-primary m-2 p-0 w-full sm:w-30" type="submit">Save</button>
|
||||||
|
</div>
|
||||||
|
<div class="divider m-0"></div>
|
||||||
|
<RecordExplanation recordType={recordDraft.record_type} name={recordDraft.name} value={recordDraft.value} displayName={getDisplayName(recordDraft.record_type, recordDraft.name)} displayValue={getDisplayValue(recordDraft.record_type, recordDraft.value, fqdn)} {fqdn}/>
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="mt-3 h-12 flex items-center">
|
||||||
|
<ErrorPopup {errorMessage} duration={10000} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
108
src/lib/RecordExplanation.svelte
Normal file
108
src/lib/RecordExplanation.svelte
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script>
|
||||||
|
let { recordType, name, value, displayName, displayValue, fqdn } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="text m-2 mr-auto max-w-fit">
|
||||||
|
{#if recordType === 'A' || recordType === 'AAAA'}
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if name}
|
||||||
|
{displayName}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayName}</code>
|
||||||
|
{/if}{fqdn}</code>
|
||||||
|
points to
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if value}
|
||||||
|
{displayValue}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayValue}</code>
|
||||||
|
{/if}</code>
|
||||||
|
{:else if recordType === 'CNAME'}
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if name}
|
||||||
|
{displayName}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayName}</code>
|
||||||
|
{/if}{fqdn}</code>
|
||||||
|
redirects to
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if value}
|
||||||
|
{displayValue}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayValue}</code>
|
||||||
|
{/if}</code>
|
||||||
|
{:else if recordType === 'NS'}
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if name}
|
||||||
|
{displayName}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayName}</code>
|
||||||
|
{/if}{fqdn}</code>
|
||||||
|
has the nameserver
|
||||||
|
<code class="bg-base-300">
|
||||||
|
{#if value}
|
||||||
|
{displayValue}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayValue}</code>
|
||||||
|
{/if}</code>
|
||||||
|
{:else if recordType === 'CNAME'}
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if name}
|
||||||
|
{displayName}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayName}</code>
|
||||||
|
{/if}{fqdn}</code>
|
||||||
|
redirects to
|
||||||
|
<code class="bg-base-300">
|
||||||
|
{#if value}
|
||||||
|
{displayValue}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayValue}</code>
|
||||||
|
{/if}</code>
|
||||||
|
{:else if recordType === 'MX'}
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if name}
|
||||||
|
{displayName}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayName}</code>
|
||||||
|
{/if}{fqdn}</code>
|
||||||
|
has the mailserver
|
||||||
|
<code class="bg-base-300">
|
||||||
|
{#if value}
|
||||||
|
{displayValue}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayValue}</code>
|
||||||
|
{/if}</code>
|
||||||
|
{:else if recordType === 'SRV'}
|
||||||
|
A service of
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if name}
|
||||||
|
{displayName}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayName}</code>
|
||||||
|
{/if}{fqdn}</code>
|
||||||
|
is hosted at
|
||||||
|
<code class="bg-base-300">
|
||||||
|
{#if value}
|
||||||
|
{displayValue}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayValue}</code>
|
||||||
|
{/if}</code>
|
||||||
|
{:else if recordType === 'TXT'}
|
||||||
|
<code class="bg-base-300 text-accent">
|
||||||
|
{#if name}
|
||||||
|
{displayName}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayName}</code>
|
||||||
|
{/if}{fqdn}</code>
|
||||||
|
has the content
|
||||||
|
<code class="bg-base-300">
|
||||||
|
{#if value}
|
||||||
|
{displayValue}
|
||||||
|
{:else}
|
||||||
|
<code class="text-primary-content/70">{displayValue}</code>
|
||||||
|
{/if}</code>
|
||||||
|
{:else}
|
||||||
|
Unknown record type
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
35
src/lib/SideMenu.svelte
Normal file
35
src/lib/SideMenu.svelte
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { domains } from "./domains.svelte";
|
||||||
|
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside class="bg-base-200">
|
||||||
|
<ul class="menu overflow-y-auto h-full w-0 sm:w-60 hidden lg:block max-h-full scroll">
|
||||||
|
<li>
|
||||||
|
<a href="/dashboard/register-domain">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
|
Register a new domain
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{#if !domains.loadingSubdomains}
|
||||||
|
<div class="divider m-0"></div>
|
||||||
|
<li>
|
||||||
|
<details open>
|
||||||
|
<summary>Manage your DNS records</summary>
|
||||||
|
<ul>
|
||||||
|
{#each domains.subdomains as sub}
|
||||||
|
<li><a href="/dashboard/{sub.id}">
|
||||||
|
{sub.name}.{sub.domain}
|
||||||
|
</a></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
{:else if domains.loadingFailed}
|
||||||
|
<li class="bg-error rounded"><a class="text-error-content" href="/logout">Loading your domains failed. Please refresh the page or try logging out and back in</a></li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
21
src/lib/api.ts
Normal file
21
src/lib/api.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { setUserLoggedOut } from "./auth.svelte.ts";
|
||||||
|
|
||||||
|
export async function get_request<T = any>(url: string): Promise<T> {
|
||||||
|
try {
|
||||||
|
let res: Response = await fetch(url, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
setUserLoggedOut();
|
||||||
|
goto("/login");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/lib/assets/favicon.svg
Normal file
14
src/lib/assets/favicon.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="107"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 107 128"
|
||||||
|
>
|
||||||
|
<title>svelte-logo</title><path
|
||||||
|
d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116"
|
||||||
|
style="fill: #ff3e00"
|
||||||
|
/><path
|
||||||
|
d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328"
|
||||||
|
style="fill: #fff"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
40
src/lib/auth.svelte.ts
Normal file
40
src/lib/auth.svelte.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
|
||||||
|
interface Auth {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isMfaEnabled?: boolean;
|
||||||
|
userEmail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let auth = $state<Auth>({
|
||||||
|
isAuthenticated: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call upon app startup. Makes a GET request to /user/me to check if the session token is present/valid
|
||||||
|
export async function initUserAuthStatus() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(PUBLIC_BACKEND_API_HOST + "/api/v1/user/me", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
setUserLoggedOut();
|
||||||
|
} else if (res.status === 200) {
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
auth.isAuthenticated = true;
|
||||||
|
auth.isMfaEnabled = data.mfa_enabled ?? false;
|
||||||
|
auth.userEmail = data.email ?? "error-email@example.com";
|
||||||
|
} else {
|
||||||
|
console.log(`Unexpected response from server: ${res.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("unexpected error: ", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUserLoggedOut() {
|
||||||
|
auth.isAuthenticated = false;
|
||||||
|
auth.isMfaEnabled = undefined;
|
||||||
|
auth.userEmail = undefined;
|
||||||
|
}
|
||||||
82
src/lib/domains.svelte.ts
Normal file
82
src/lib/domains.svelte.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
|
||||||
|
export type Subdomain = {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BaseDomain = {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Domains {
|
||||||
|
loadingBaseDomains: boolean;
|
||||||
|
loadingSubdomains: boolean;
|
||||||
|
subdomains: Subdomain[];
|
||||||
|
subdomainsFromId: Record<string, Subdomain>,
|
||||||
|
baseDomains: BaseDomain[];
|
||||||
|
loadingFailed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let domains = $state<Domains>({
|
||||||
|
loadingBaseDomains: true,
|
||||||
|
loadingSubdomains: true,
|
||||||
|
subdomains: [],
|
||||||
|
subdomainsFromId: {},
|
||||||
|
baseDomains: [],
|
||||||
|
loadingFailed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function clearSubdomains() {
|
||||||
|
domains.subdomains = [];
|
||||||
|
domains.loadingFailed = false;
|
||||||
|
domains.loadingSubdomains = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getSubdomains() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${PUBLIC_BACKEND_API_HOST}/api/v1/subdomain`
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data?.msg || "Failed to load subdomains");
|
||||||
|
}
|
||||||
|
domains.subdomains = data
|
||||||
|
.slice()
|
||||||
|
.sort((a: { id: string; }, b: { id: any; }) => a.id.localeCompare(b.id));
|
||||||
|
|
||||||
|
for (const sub of domains.subdomains) {
|
||||||
|
domains.subdomainsFromId[sub.id] = sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
domains.loadingFailed = true;
|
||||||
|
} finally {
|
||||||
|
domains.loadingSubdomains = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getBaseDomains() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${PUBLIC_BACKEND_API_HOST}/api/v1/subdomain/domains`
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data?.msg || "Failed to load base domains");
|
||||||
|
}
|
||||||
|
domains.baseDomains = data
|
||||||
|
.slice()
|
||||||
|
.sort((a: { id: string; }, b: { id: any; }) => a.id.localeCompare(b.id));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
domains.loadingFailed = true;
|
||||||
|
} finally {
|
||||||
|
domains.loadingBaseDomains = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
175
src/lib/records.svelte.ts
Normal file
175
src/lib/records.svelte.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from '$env/static/public';
|
||||||
|
|
||||||
|
export interface DnsRecord {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
record_type: string;
|
||||||
|
value: string;
|
||||||
|
ttl: number;
|
||||||
|
comment: string;
|
||||||
|
is_active: boolean;
|
||||||
|
priority?: number;
|
||||||
|
weight?: number;
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordsResponse {
|
||||||
|
msg: string;
|
||||||
|
records: DnsRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRecords(
|
||||||
|
subdomainId: string
|
||||||
|
): Promise<RecordsResponse> {
|
||||||
|
const res = await fetch(
|
||||||
|
`${PUBLIC_BACKEND_API_HOST}/api/v1/record/${subdomainId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Failed to fetch records');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValueTitleText(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'A': return 'IPv4 address'
|
||||||
|
case 'AAAA': return 'IPv6 address'
|
||||||
|
case 'CNAME': return 'Target'
|
||||||
|
case 'NS': return 'Nameserver'
|
||||||
|
case 'MX': return 'Mailserver'
|
||||||
|
case 'SRV': return 'Target'
|
||||||
|
case 'TXT': return 'Content'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNamePlaceholderText(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'A': return 'example'
|
||||||
|
case 'AAAA': return 'example'
|
||||||
|
case 'CNAME': return 'www'
|
||||||
|
case 'NS': return 'example'
|
||||||
|
case 'MX': return 'example'
|
||||||
|
case 'SRV': return '_service._proto'
|
||||||
|
case 'TXT': return 'example'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValuePlaceholderText(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'A': return '198.51.100.51'
|
||||||
|
case 'AAAA': return 'fd40:7d1d:b637::'
|
||||||
|
case 'CNAME': return 'example.com'
|
||||||
|
case 'NS': return 'ns.example.com'
|
||||||
|
case 'MX': return 'mx.example.com'
|
||||||
|
case 'SRV': return 'example.com'
|
||||||
|
case 'TXT': return 'v=spf1 mx -all'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisplayName(type: string, name: string) {
|
||||||
|
if (!name) {
|
||||||
|
return `${getNamePlaceholderText(type)}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "@") {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return `${name}.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisplayValue(type: string, value: string, fqdn: string) {
|
||||||
|
if (!value) {
|
||||||
|
return `${getValuePlaceholderText(type)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "@" && (type === 'MX' || type === 'CNAME' || type === 'NS')) {
|
||||||
|
return fqdn;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRecordValid(
|
||||||
|
recordType: string,
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
priority: number,
|
||||||
|
weight: number,
|
||||||
|
port: number,
|
||||||
|
comment: string,
|
||||||
|
ttl: number
|
||||||
|
): string {
|
||||||
|
if (comment.length >= 255) {
|
||||||
|
return 'The comment length must not exceed 255 characters'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ttl < 30 || ttl > 2147483647) {
|
||||||
|
return 'The TTL must not be below 30 and above 2147483647'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name.match(/^[a-zA-Z0-9-_.]+$/) && name !== '@') {
|
||||||
|
return 'Invalid record name';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (recordType) {
|
||||||
|
case 'A':
|
||||||
|
if (!value.match(/^(?:\d{1,3}\.){3}\d{1,3}$/)) {
|
||||||
|
return 'Invalid IPv4 address';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'AAAA':
|
||||||
|
if (!value.match(/^[0-9a-fA-F:]+$/)) {
|
||||||
|
return 'Invalid IPv6 address';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'CNAME':
|
||||||
|
case 'NS':
|
||||||
|
if (!value.match(/^[a-zA-Z0-9.-]+$/) && name === '@') {
|
||||||
|
return 'Target must be a fully qualified domain name';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'MX':
|
||||||
|
if (priority < 0) {
|
||||||
|
return 'MX records require a non-negative priority';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'SRV':
|
||||||
|
if (priority < 0 || weight < 0 || port < 0 ||
|
||||||
|
priority > 65535 || weight > 65535 || port > 65535
|
||||||
|
) {
|
||||||
|
return 'Priority, weight, and port must be in the range of 0-65535';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'TXT':
|
||||||
|
if (value.length === 0 || value.length > 255) {
|
||||||
|
return 'The content of the TXT record must not exceed 255 characters';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes sure to update/replace records which have been implicitly updated by either a TTL change or have been created
|
||||||
|
// Find all records with the same name and type, delete them, insert the records from the API response
|
||||||
|
export function handleUiRecordUpdates(oldRecords: DnsRecord[], data: any): DnsRecord[] {
|
||||||
|
let records = oldRecords.filter((rec) => rec.record_type !== data.type || rec.name !== data.name);
|
||||||
|
for (let rec of data.records) {
|
||||||
|
let newRecord: DnsRecord = {
|
||||||
|
id: rec.id,
|
||||||
|
name: data.name,
|
||||||
|
record_type: data.type,
|
||||||
|
value: rec.value,
|
||||||
|
ttl: data.ttl,
|
||||||
|
comment: rec.comment,
|
||||||
|
is_active: rec.is_active,
|
||||||
|
priority: rec.priority,
|
||||||
|
weight: rec.weight,
|
||||||
|
port: rec.port,
|
||||||
|
};
|
||||||
|
records.push(newRecord);
|
||||||
|
}
|
||||||
|
return records.sort((a, b) => (a.id > b.id ? -1 : 1));
|
||||||
|
}
|
||||||
39
src/routes/+layout.svelte
Normal file
39
src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import './layout.css';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from '$env/static/public';
|
||||||
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
import MainNavbar from '$lib/MainNavbar.svelte';
|
||||||
|
import SideMenu from '$lib/SideMenu.svelte';
|
||||||
|
import { auth, initUserAuthStatus } from '$lib/auth.svelte';
|
||||||
|
import { domains, getSubdomains, getBaseDomains } from '$lib/domains.svelte';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await initUserAuthStatus();
|
||||||
|
getBaseDomains();
|
||||||
|
|
||||||
|
if (auth.isAuthenticated) {
|
||||||
|
getSubdomains();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="icon" href={favicon}/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-screen">
|
||||||
|
<MainNavbar/>
|
||||||
|
<div class="flex flex-row min-h-0 flex-grow">
|
||||||
|
{#if auth.isAuthenticated}
|
||||||
|
<SideMenu/>
|
||||||
|
<!-- {:else}
|
||||||
|
<SideMenu/> -->
|
||||||
|
{/if}
|
||||||
|
<div class="flex-grow overflow-auto p-2 sm:p-12">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
272
src/routes/+page.svelte
Normal file
272
src/routes/+page.svelte
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
<!-- <main class="px-6 md:px-16 lg:px-24 xl:px-32"> -->
|
||||||
|
<svg class="relative bg-black -translate-y-12 -z-10 w-full -mt-40 md:mt-0" width="1440" height="676" viewBox="0 0 1440 676" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity: 1;">
|
||||||
|
<rect x="-92" y="-948" width="1624" height="1624" rx="812" fill="url(#a)"></rect>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="a" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(90 428 292)scale(812)">
|
||||||
|
<stop offset=".63" stop-color="#1244e3" stop-opacity="0"></stop>
|
||||||
|
<stop offset="1" stop-color="#1244e3"></stop>
|
||||||
|
<!-- #f43098 -->
|
||||||
|
<!-- #00d3bb -->
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<section class="flex flex-col items-center -mt-18 -translate-y-170">
|
||||||
|
<a class="flex items-center mt-48 gap-2 border border-slate-600 text-gray-50 rounded-full px-4 py-2" style="opacity: 1; transform: none;">
|
||||||
|
<div class="size-2.5 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
|
<span>Start using today!</span>
|
||||||
|
</a>
|
||||||
|
<h1 class="text-center text-5xl leading-[68px] md:text-6xl md:leading-[70px] mt-4 font-semibold max-w-3xl" style="opacity: 1; transform: none;">Let's manage the backbone of every website - DNS</h1>
|
||||||
|
<p class="text-center text-base max-w-lg mt-2" style="opacity: 1; transform: none;">Our platform helps you manage DNS and DynDNS - so you can focus on what really matters.</p>
|
||||||
|
<div class="flex items-center gap-4 mt-8" style="opacity: 1; transform: none;">
|
||||||
|
<!-- <button class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 transition text-white active:scale-95 rounded-lg px-7 h-11"> -->
|
||||||
|
<button class="btn btn-lg btn-primary rounded-lg w-50">
|
||||||
|
Get started
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right size-5" aria-hidden="true">
|
||||||
|
<path d="M5 12h14"></path>
|
||||||
|
<path d="m12 5 7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-lg btn-outline btn-secondary rounded-lg w-50">Contact us</button>
|
||||||
|
</div>
|
||||||
|
<figure class="relative w-full h-full [perspective:800px] mt-16 max-w-4xl mx-auto flex flex-col items-center justify-center" style="opacity: 1; transform: none;">
|
||||||
|
<!-- <div class="relative [transform-style:preserve-3d] w-full max-w-4xl" style="transform: none;"><img src="https://raw.githubusercontent.com/prebuiltui/prebuiltui/main/assets/hero/hero-section-showcase-2.png" class="w-full rounded-[15px] will-change-transform [transform:translateZ(0)]" alt="hero section showcase"></div> -->
|
||||||
|
<div class="mockup-code w-xl my-8 overflow-hidden">
|
||||||
|
<pre data-prefix="$"><code>dig example.hexname.com</code></pre>
|
||||||
|
<!-- <pre class="text-success translate-x-2"><code>; <<>> DiG 9.20.16 <<>> example.hexname.com</code></pre> -->
|
||||||
|
<!-- <pre class="text-success translate-x-2"><code>;; global options: +cmd</code></pre>
|
||||||
|
<pre class="text-success translate-x-2"><code>;; Got answer:</code></pre>
|
||||||
|
<pre class="text-success translate-x-2"><code>;; - >>HEADER<<- opcode: QUERY, status: NOERROR, id: 47911</code></pre>
|
||||||
|
<pre class="text-success translate-x-2"><code>;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1</code></pre> -->
|
||||||
|
<!-- <pre class="text-success translate-x-2"><code>;; WARNING: recursion requested but not available</code></pre> -->
|
||||||
|
<!-- <pre></pre>
|
||||||
|
<pre class="text-success translate-x-2"><code>;; OPT PSEUDOSECTION:</code></pre>
|
||||||
|
<pre class="text-success translate-x-2"><code>; EDNS: version: 0, flags:; udp: 1232</code></pre> -->
|
||||||
|
<pre class="text-secondary translate-x-2"><code>;; QUESTION SECTION:</code></pre>
|
||||||
|
<pre class="text-secondary translate-x-2"><code>;example.hexname.com. IN A</code></pre>
|
||||||
|
<pre></pre>
|
||||||
|
<pre class="text-secondary translate-x-2"><code>;; ANSWER SECTION:</code></pre>
|
||||||
|
<pre class="text-accent translate-x-2"><code>example.hexname.com. 300 IN A 198.51.100.51</code></pre>
|
||||||
|
<pre class="text-secondary translate-x-2"><code>;; Query time: 57 msec</code></pre>
|
||||||
|
<pre class="text-secondary translate-x-2"><code>;; SERVER: 9.9.9.9#53(9.9.9.9) (UDP)</code></pre>
|
||||||
|
<!-- <pre class="text-success translate-x-2"><code>;; WHEN: Mon Jan 05 16:50:33 CET 2026</code></pre> -->
|
||||||
|
<!-- <pre class="text-success translate-x-2"><code>;; MSG SIZE rcvd: 63</code></pre> -->
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
<section class="flex flex-col items-center" id="creations">
|
||||||
|
<div class="flex flex-col items-center mt-32">
|
||||||
|
<h2 class="text-center text-4xl font-semibold max-w-2xl" style="opacity: 1; transform: none;">
|
||||||
|
Our <span class="bg-gradient-to-t from-indigo-600 to-black p-1 bg-left inline-block bg-no-repeat" style="background-size: 100% 100%;">DNS services</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-center text-slate-400 max-w-lg mt-3" style="opacity: 1; transform: none;">Register our premium subdomains and manage
|
||||||
|
<code class="text-secondary font-mono">A</code>,
|
||||||
|
<code class="text-secondary font-mono">AAAA</code>,
|
||||||
|
<code class="text-secondary font-mono">TXT</code>,
|
||||||
|
<code class="text-secondary font-mono">CNAME</code>,
|
||||||
|
<code class="text-secondary font-mono">MX</code>,
|
||||||
|
<code class="text-secondary font-mono">NS</code>,
|
||||||
|
<code class="text-secondary font-mono">SRV</code>
|
||||||
|
records.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 h-100 w-full max-w-5xl mt-18 mx-auto">
|
||||||
|
<div class="relative group flex-grow h-[400px] rounded-xl overflow-hidden w-56 transition-all duration-500 " style="opacity: 1; transform: none;">
|
||||||
|
<img class="h-full w-full object-cover object-center" src="https://images.unsplash.com/photo-1543269865-0a740d43b90c?q=80&w=800&h=400&auto=format&fit=crop" alt="Prompt engineers">
|
||||||
|
<div class="absolute inset-0 flex flex-col justify-end p-10 text-white bg-black/50 transition-all duration-300 opacity-0">
|
||||||
|
<h1 class="text-3xl font-semibold">Prompt engineers</h1>
|
||||||
|
<p class="text-sm mt-2">Bridging the gap between human intent and machine understanding through expert prompt design.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative group flex-grow h-[400px] rounded-xl overflow-hidden w-full transition-all duration-500 " style="opacity: 1; transform: none;">
|
||||||
|
<img class="h-full w-full object-cover object-right" src="https://images.unsplash.com/photo-1714976326351-0ecf0244f0fc?q=80&w=800&h=400&auto=format&fit=crop" alt="Data scientists">
|
||||||
|
<div class="absolute inset-0 flex flex-col justify-end p-10 text-white bg-black/50 transition-all duration-300 opacity-100">
|
||||||
|
<h1 class="text-3xl font-semibold">Data scientists</h1>
|
||||||
|
<p class="text-sm mt-2">Turning data into actionable insights that drive intelligent innovation and growth.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative group flex-grow h-[400px] rounded-xl overflow-hidden w-56 transition-all duration-500 " style="opacity: 1; transform: none;">
|
||||||
|
<img class="h-full w-full object-cover object-center" src="https://images.unsplash.com/photo-1736220690062-79e12ca75262?q=80&w=800&h=400&auto=format&fit=crop" alt="Software engineers">
|
||||||
|
<div class="absolute inset-0 flex flex-col justify-end p-10 text-white bg-black/50 transition-all duration-300 opacity-0">
|
||||||
|
<h1 class="text-3xl font-semibold">Software engineers</h1>
|
||||||
|
<p class="text-sm mt-2">Building scalable and efficient systems that bring ideas to life through code.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="flex flex-col items-center" id="about">
|
||||||
|
<div class="flex flex-col items-center mt-32">
|
||||||
|
<h2 class="text-center text-4xl font-semibold max-w-2xl" style="opacity: 1; transform: none;">
|
||||||
|
About our<!-- --> <span class="bg-gradient-to-t from-indigo-600 to-black p-1 bg-left inline-block bg-no-repeat" style="background-size: 100% 100%;">apps</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-center text-slate-400 max-w-lg mt-3" style="opacity: 1; transform: none;">A visual collection of our most recent works - each piece crafted with intention, emotion, and style.</p>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-w-5xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 px-8 md:px-0 mt-18">
|
||||||
|
<div class="py-10 border-b border-slate-700 md:py-0 md:border-r md:border-b-0 md:px-10" style="opacity: 1; transform: none;">
|
||||||
|
<div class="size-10 p-2 bg-indigo-600/20 border border-indigo-600/30 rounded"><img src="https://raw.githubusercontent.com/prebuiltui/prebuiltui/main/assets/aboutSection/flashEmoji.png" alt=""></div>
|
||||||
|
<div class="mt-5 space-y-2">
|
||||||
|
<h3 class="text-base font-medium text-slate-200">Lightning-Fast Performance</h3>
|
||||||
|
<p class="text-sm text-slate-400">Built with speed — minimal load times and optimized.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="py-10 border-b border-slate-700 md:py-0 lg:border-r md:border-b-0 md:px-10" style="opacity: 1; transform: none;">
|
||||||
|
<div class="size-10 p-2 bg-indigo-600/20 border border-indigo-600/30 rounded"><img src="https://raw.githubusercontent.com/prebuiltui/prebuiltui/main/assets/aboutSection/colorsEmoji.png" alt=""></div>
|
||||||
|
<div class="mt-5 space-y-2">
|
||||||
|
<h3 class="text-base font-medium text-slate-200">Beautifully Designed Components</h3>
|
||||||
|
<p class="text-sm text-slate-400">Modern, pixel-perfect UI components ready for any project.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="py-10 border-b border-slate-700 md:py-0 md:border-b-0 md:px-10" style="opacity: 1; transform: none;">
|
||||||
|
<div class="size-10 p-2 bg-indigo-600/20 border border-indigo-600/30 rounded"><img src="https://raw.githubusercontent.com/prebuiltui/prebuiltui/main/assets/aboutSection/puzzelEmoji.png" alt=""></div>
|
||||||
|
<div class="mt-5 space-y-2">
|
||||||
|
<h3 class="text-base font-medium text-slate-200">Plug-and-Play Integration</h3>
|
||||||
|
<p class="text-sm text-slate-400">Simple setup with support for React, Next.js and Tailwind css.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="flex flex-col items-center" id="testimonials">
|
||||||
|
<div class="flex flex-col items-center mt-32">
|
||||||
|
<h2 class="text-center text-4xl font-semibold max-w-2xl" style="opacity: 1; transform: none;">
|
||||||
|
Our<!-- --> <span class="bg-gradient-to-t from-indigo-600 to-black p-1 bg-left inline-block bg-no-repeat" style="background-size: 100% 100%;">testimonials</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-center text-slate-400 max-w-lg mt-3" style="opacity: 1; transform: none;">A visual collection of our most recent works - each piece crafted with intention, emotion, and style.</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-18 max-w-6xl mx-auto">
|
||||||
|
<div class="group border border-slate-800 p-6 rounded-xl" style="opacity: 1; transform: none;">
|
||||||
|
<p class="text-slate-100 text-base">Super clean and easy to use. These Tailwind + React components saved me hours of dev time!</p>
|
||||||
|
<div class="flex items-center gap-3 mt-8 group-hover:-translate-y-1 duration-300">
|
||||||
|
<img class="size-10 rounded-full" src="https://images.unsplash.com/photo-1633332755192-727a05c4013d?q=80&w=200" alt="user image">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-gray-200 font-medium">Richard Nelson</h2>
|
||||||
|
<p class="text-indigo-500">AI Content Marketer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group border border-slate-800 p-6 rounded-xl" style="opacity: 1; transform: none;">
|
||||||
|
<p class="text-slate-100 text-base">The design quality is top-notch. Perfect balance between simplicity and style. Highly recommend!</p>
|
||||||
|
<div class="flex items-center gap-3 mt-8 group-hover:-translate-y-1 duration-300">
|
||||||
|
<img class="size-10 rounded-full" src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=200" alt="user image">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-gray-200 font-medium">Sophia Martinez</h2>
|
||||||
|
<p class="text-indigo-500">UI/UX Designer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group border border-slate-800 p-6 rounded-xl" style="opacity: 1; transform: none;">
|
||||||
|
<p class="text-slate-100 text-base">Absolutely love the reusability of these components. My workflow feels 10x faster now.</p>
|
||||||
|
<div class="flex items-center gap-3 mt-8 group-hover:-translate-y-1 duration-300">
|
||||||
|
<img class="size-10 rounded-full" src="https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=200&auto=format&fit=crop&q=60" alt="user image">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-gray-200 font-medium">Ethan Roberts</h2>
|
||||||
|
<p class="text-indigo-500">Frontend Developer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group border border-slate-800 p-6 rounded-xl" style="opacity: 1; transform: none;">
|
||||||
|
<p class="text-slate-100 text-base">Clean, elegant, and efficient. These components are a dream for any modern web developer.</p>
|
||||||
|
<div class="flex items-center gap-3 mt-8 group-hover:-translate-y-1 duration-300">
|
||||||
|
<img class="size-10 rounded-full" src="https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=200&auto=format&fit=crop&q=60" alt="user image">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-gray-200 font-medium">Isabella Kim</h2>
|
||||||
|
<p class="text-indigo-500">Product Designer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group border border-slate-800 p-6 rounded-xl" style="opacity: 1; transform: none;">
|
||||||
|
<p class="text-slate-100 text-base">I've tried dozens of UI kits, but this one just feels right. Everything works seamlessly.</p>
|
||||||
|
<div class="flex items-center gap-3 mt-8 group-hover:-translate-y-1 duration-300">
|
||||||
|
<img class="size-10 rounded-full" src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=100&h=100&auto=format&fit=crop" alt="user image">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-gray-200 font-medium">Liam Johnson</h2>
|
||||||
|
<p class="text-indigo-500">Software Engineer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group border border-slate-800 p-6 rounded-xl" style="opacity: 1; transform: none;">
|
||||||
|
<p class="text-slate-100 text-base">Brilliantly structured components with clean, modern styling. Makes development a joy!</p>
|
||||||
|
<div class="flex items-center gap-3 mt-8 group-hover:-translate-y-1 duration-300">
|
||||||
|
<img class="size-10 rounded-full" src="https://raw.githubusercontent.com/prebuiltui/prebuiltui/main/assets/userImage/userImage1.png" alt="user image">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-gray-200 font-medium">Ava Patel</h2>
|
||||||
|
<p class="text-indigo-500">Full Stack Developer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="flex flex-col items-center">
|
||||||
|
<div class="flex flex-col items-center mt-32">
|
||||||
|
<h2 class="text-center text-4xl font-semibold max-w-2xl" style="opacity: 1; transform: none;">
|
||||||
|
Trusted<!-- --> <span class="bg-gradient-to-t from-indigo-600 to-black p-1 bg-left inline-block bg-no-repeat" style="background-size: 100% 100%;">companies</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-center text-slate-400 max-w-lg mt-3" style="opacity: 1; transform: none;">A visual collection of our most recent works - each piece crafted with intention, emotion, and style.</p>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-w-5xl py-20 md:py-26 mt-18 md:w-full overflow-hidden mx-2 md:mx-auto border border-indigo-900 flex flex-col md:flex-row items-center justify-between bg-gradient-to-br from-[#401B98]/5 to-[#180027]/10 rounded-3xl p-4 md:p-10 text-white" style="opacity: 1; transform: none;">
|
||||||
|
<div class="absolute pointer-events-none top-10 -z-1 left-20 size-64 bg-gradient-to-br from-[#536DFF] to-[#4F39F6]/60 blur-[180px]"></div>
|
||||||
|
<div class="absolute pointer-events-none bottom-10 -z-1 right-20 size-64 bg-gradient-to-br from-[#536DFF] to-[#4F39F6]/60 blur-[180px]"></div>
|
||||||
|
<div class="flex flex-col items-center md:items-start max-md:text-center">
|
||||||
|
<a href="https://prebuiltui.com" class="group flex items-center gap-2 rounded-full text-sm p-1 pr-3 text-indigo-300 bg-indigo-200/15">
|
||||||
|
<span class="bg-indigo-600 text-white text-xs px-3.5 py-1 rounded-full">NEW</span>
|
||||||
|
<p class="flex items-center gap-1">
|
||||||
|
<span>Try 30 days free trial option </span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon lucide-chevron-right group-hover:translate-x-0.5 transition duration-300">
|
||||||
|
<path d="m9 18 6-6-6-6"></path>
|
||||||
|
</svg>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<h1 class="text-3xl font-medium max-w-xl mt-5 bg-gradient-to-r from-white to-[#b6abff] text-transparent bg-clip-text">Trusted by leading companies.</h1>
|
||||||
|
<p class="text-base text-slate-400 max-w-lg mt-4">Built to integrate effortlessly with your existing tools, frameworks and workflows — so you can move faster.</p>
|
||||||
|
<button class="flex items-center gap-1 text-sm px-6 py-2.5 border border-indigo-400 hover:bg-indigo-300/10 active:scale-95 transition rounded-full mt-6">
|
||||||
|
Read more
|
||||||
|
<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.4243 5.42426C12.6586 5.18995 12.6586 4.81005 12.4243 4.57574L8.60589 0.757359C8.37157 0.523045 7.99167 0.523045 7.75736 0.757359C7.52304 0.991674 7.52304 1.37157 7.75736 1.60589L11.1515 5L7.75736 8.39411C7.52304 8.62843 7.52304 9.00833 7.75736 9.24264C7.99167 9.47696 8.37157 9.47696 8.60589 9.24264L12.4243 5.42426ZM0 5L0 5.6L12 5.6V5V4.4L0 4.4L0 5Z" fill="white"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="md:-mr-16 max-md:mt-10"><img class="max-w-xs md:max-w-sm" src="https://raw.githubusercontent.com/prebuiltui/prebuiltui/main/assets/trusted-brand/image-fc6e.png" alt=""></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="flex flex-col items-center" id="contact">
|
||||||
|
<div class="flex flex-col items-center mt-32">
|
||||||
|
<h2 class="text-center text-4xl font-semibold max-w-2xl" style="opacity: 1; transform: none;">
|
||||||
|
Get in<!-- --> <span class="bg-gradient-to-t from-indigo-600 to-black p-1 bg-left inline-block bg-no-repeat" style="background-size: 100% 100%;">touch</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-center text-slate-400 max-w-lg mt-3" style="opacity: 1; transform: none;">A visual collection of our most recent works - each piece crafted with intention, emotion, and style.</p>
|
||||||
|
</div>
|
||||||
|
<form class="grid sm:grid-cols-2 gap-3 sm:gap-5 max-w-3xl mx-auto text-slate-400 mt-16 w-full">
|
||||||
|
<div style="opacity: 1; transform: none;"><label class="font-medium text-slate-200">Your name</label><input type="text" placeholder="Enter your name" class="w-full mt-2 p-3 outline-none border border-slate-700 rounded-lg focus-within:ring-1 transition focus:ring-indigo-600" name="name"></div>
|
||||||
|
<div style="opacity: 1; transform: none;"><label class="font-medium text-slate-200">Email id</label><input type="email" placeholder="Enter your email" class="w-full mt-2 p-3 outline-none border border-slate-700 rounded-lg focus-within:ring-1 transition focus:ring-indigo-600" name="email"></div>
|
||||||
|
<div class="sm:col-span-2" style="opacity: 1; transform: none;"><label class="font-medium text-slate-200">Message</label><textarea name="message" rows="8" placeholder="Enter your message" class="resize-none w-full mt-2 p-3 outline-none rounded-lg focus-within:ring-1 transition focus:ring-indigo-600 border border-slate-700"></textarea></div>
|
||||||
|
<button type="submit" class="w-max flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-8 py-3 rounded-full" style="opacity: 1; transform: none;">
|
||||||
|
Submit
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4.5" aria-hidden="true">
|
||||||
|
<path d="M7 7h10v10"></path>
|
||||||
|
<path d="M7 17 17 7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<section class="flex flex-col items-center">
|
||||||
|
<div class="flex flex-col items-center mt-32">
|
||||||
|
<h2 class="text-center text-4xl font-semibold max-w-2xl" style="opacity: 1; transform: none;">
|
||||||
|
Subscribe<!-- --> <span class="bg-gradient-to-t from-indigo-600 to-black p-1 bg-left inline-block bg-no-repeat" style="background-size: 100% 100%;">newsletter</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-center text-slate-400 max-w-lg mt-3" style="opacity: 1; transform: none;">A visual collection of our most recent works - each piece crafted with intention, emotion, and style.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center mt-10 border border-slate-700 focus-within:outline focus-within:outline-indigo-600 text-sm rounded-full h-14 max-w-xl w-full" style="opacity: 1; transform: none;"><input type="text" class="bg-transparent outline-none rounded-full px-4 h-full flex-1 placeholder:text-slate-400" placeholder="Enter your email address"><button class="bg-indigo-600 text-white rounded-full h-11 mr-1 px-10 flex items-center justify-center hover:bg-indigo-700 active:scale-95 transition">Subscribe</button></div>
|
||||||
|
</section>
|
||||||
|
<!-- </main> -->
|
||||||
|
|
||||||
|
<!-- <div class="flex flex-col items-center px-4 scale-40">
|
||||||
|
<div class="mockup-phone border-primary rotate-10 shadow-2xl shadow-accent ring-2 ring-accent inset-ring-2 inset-ring-accent">
|
||||||
|
<div class="mockup-phone-camera"></div>
|
||||||
|
<div class="mockup-phone-display text-white grid place-content-center bg-neutral-900 text-4xl">
|
||||||
|
<p class="text-shadow-lg text-shadow-accent text-center">
|
||||||
|
idfk what to put here
|
||||||
|
</p>
|
||||||
|
<div class="mockup-code w-full my-8">
|
||||||
|
<pre data-prefix="$"><code>dig mikes-lab.hexname.com +short</code></pre>
|
||||||
|
<pre class="text-success"><code>192.168.39.1</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
After Width: | Height: | Size: 22 KiB |
44
src/routes/about/+page.svelte
Normal file
44
src/routes/about/+page.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<div class="flex flex-col items-center min-h-full">
|
||||||
|
<div class="w-4xl">
|
||||||
|
|
||||||
|
<div class="hero bg-base-200">
|
||||||
|
<div class="hero-content flex-col lg:flex-row-reverse">
|
||||||
|
<div class="mockup-code w-110">
|
||||||
|
<pre data-prefix="$"><code>Question?</code></pre>
|
||||||
|
<pre data-prefix=">" class="text-warning"><code>Answering...</code></pre>
|
||||||
|
<pre data-prefix=">" class="text-success"><code>Done!</code></pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold">Our mission</h1>
|
||||||
|
<div class="py-6">
|
||||||
|
We saw that the available solutions for free DNS management as well as DDNS were lackluster.
|
||||||
|
We thought that this was unacceptable, and decided to create the simplest solution for managing your DNS records, with more freedom than anyone on the market.
|
||||||
|
</div>
|
||||||
|
<a href="/register" class="btn btn-primary w-40">Get started</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
<h2 class="legend text-xl font-bold p-4 m-4">About the team</h2>
|
||||||
|
HexName was developed by a team of passionate tech-enthusiasts with years of experience in the field.
|
||||||
|
|
||||||
|
<div class="hero bg-base-200">
|
||||||
|
<div class="hero-content flex-col lg:flex-row">
|
||||||
|
<img
|
||||||
|
src="https://img.daisyui.com/images/stock/photo-1635805737707-575885ab0820.webp"
|
||||||
|
class="max-w-sm rounded-lg shadow-2xl"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-3xl font-bold">Head of Marketing</h2>
|
||||||
|
<h3 class="text-md italic">Luka Dekanozishvili</h3>
|
||||||
|
<p class="py-6">
|
||||||
|
Mr. Dekanozishvili is our Head of Marketing as well as our Senior Marketing Manager. He excells in Search Engine Optimization, and helped build up the reputation of HexName from the ground up.
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-primary">Get in touch!</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
src/routes/dashboard/+page.svelte
Normal file
11
src/routes/dashboard/+page.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { auth } from '$lib/auth.svelte';
|
||||||
|
|
||||||
|
function toggleAuth() {
|
||||||
|
auth.isAuthenticated = !auth.isAuthenticated;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-full">
|
||||||
|
<button on:click={toggleAuth} class="btn btn-primary">Toggle isAuthenticated</button>
|
||||||
|
</div>
|
||||||
352
src/routes/dashboard/[id]/+page.svelte
Normal file
352
src/routes/dashboard/[id]/+page.svelte
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import { domains, type Subdomain } from '$lib/domains.svelte';
|
||||||
|
import ErrorPopup from '$lib/ErrorPopup.svelte';
|
||||||
|
import Record from '$lib/Record.svelte';
|
||||||
|
import RecordExplanation from '$lib/RecordExplanation.svelte';
|
||||||
|
import { fetchRecords, getDisplayName, getDisplayValue, getNamePlaceholderText, getValuePlaceholderText, getValueTitleText, handleUiRecordUpdates, isRecordValid, type DnsRecord } from '$lib/records.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let recordType: string = $state('A');
|
||||||
|
let name = $state('');
|
||||||
|
let value = $state('');
|
||||||
|
let priority: number = $state(10);
|
||||||
|
let weight: number = $state(5);
|
||||||
|
let port: number = $state(8080);
|
||||||
|
let comment = $state('');
|
||||||
|
let error = $state('');
|
||||||
|
let recordsError = $state('');
|
||||||
|
let ttl = $state(300);
|
||||||
|
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!domains.loadingSubdomains && !subdomain) {
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
(async () => {
|
||||||
|
const id = page.params.id;
|
||||||
|
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
loadingRecords = true;
|
||||||
|
recordsError = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchRecords(id);
|
||||||
|
records = data.records.sort((a, b) => (a.id > b.id ? -1 : 1));
|
||||||
|
} catch (e) {
|
||||||
|
recordsError = (e as Error).message;
|
||||||
|
records = [];
|
||||||
|
} finally {
|
||||||
|
loadingRecords = false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
let records = $state<DnsRecord[]>([]);
|
||||||
|
let loadingRecords = $state(true);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchRecords(page.params.id ?? '');
|
||||||
|
records = data.records.sort((a, b) => (a.id > b.id ? -1 : 1));
|
||||||
|
} catch (e) {
|
||||||
|
error = (e as Error).message;
|
||||||
|
} finally {
|
||||||
|
loadingRecords = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const subdomain = $derived.by<Subdomain>(() => {
|
||||||
|
return domains.subdomainsFromId[page.params.id ?? ''];
|
||||||
|
});
|
||||||
|
|
||||||
|
const fqdn = $derived.by(() => {
|
||||||
|
return subdomain ? `${subdomain.name}.${subdomain.domain}` : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
errorMessage = isRecordValid(recordType, name, value, priority, weight, port, comment, ttl);
|
||||||
|
if (errorMessage !== '') {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let processedName = name === '@' ? '' : name
|
||||||
|
|
||||||
|
try {
|
||||||
|
let res;
|
||||||
|
|
||||||
|
if (recordType === "MX") {
|
||||||
|
let processedValue = value === '@' ? fqdn : value;
|
||||||
|
res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"name": processedName, "value": processedValue, ttl, comment, "record_type": recordType, "subdomain_id": page.params.id, priority})
|
||||||
|
});
|
||||||
|
} else if (recordType === "SRV") {
|
||||||
|
let processedValue = value === '@' ? fqdn : value;
|
||||||
|
res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"name": processedName, "value": processedValue, ttl, comment, "record_type": recordType, "subdomain_id": page.params.id, priority, weight, port})
|
||||||
|
});
|
||||||
|
} else if (recordType === "NS" || recordType === "CNAME") {
|
||||||
|
let processedValue = value === '@' ? fqdn : value;
|
||||||
|
res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"name": processedName, "value": processedValue, ttl, comment, "record_type": recordType, "subdomain_id": page.params.id })
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/record`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"name": processedName, value, ttl, comment, "record_type": recordType, "subdomain_id": page.params.id })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
console.log(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
records = handleUiRecordUpdates(records, data);
|
||||||
|
alert("Record successfully created!");
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
console.log(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueTitleText = $derived.by(() => {
|
||||||
|
return getValueTitleText(recordType)
|
||||||
|
})
|
||||||
|
|
||||||
|
let namePlaceholderText = $derived.by(() => {
|
||||||
|
return getNamePlaceholderText(recordType)
|
||||||
|
})
|
||||||
|
|
||||||
|
let valuePlaceholderText = $derived.by(() => {
|
||||||
|
return getValuePlaceholderText(recordType)
|
||||||
|
})
|
||||||
|
|
||||||
|
let displayName = $derived.by(() => {
|
||||||
|
return getDisplayName(recordType, name)
|
||||||
|
});
|
||||||
|
|
||||||
|
let displayValue = $derived.by(() => {
|
||||||
|
return getDisplayValue(recordType, value, fqdn);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center px-4 w-full">
|
||||||
|
<form class="bg-base-200 border-base-300 rounded-box w-full max-w-6xl border p-4 z-3" onsubmit={handleSubmit}>
|
||||||
|
<div class="sm:flex sm:flex-col sm:flex-row">
|
||||||
|
<legend class="text-lg m-2 font-semibold">Create a new Record</legend>
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column ml-auto">
|
||||||
|
<svg class="stroke-info sm:m-0 m-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
<span class="text-xs text-info flex-1">Use '@' to signify your entire domain</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row justify-center">
|
||||||
|
<div class="flex flex-col sm:flex-row">
|
||||||
|
<div class="form-control m-2 sm:w-30">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<label class="fieldset-legend" for="record-type">Type</label>
|
||||||
|
<select id="record-type" bind:value={recordType} class="select select-bordered w-full">
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="AAAA">AAAA</option>
|
||||||
|
<option value="TXT">TXT</option>
|
||||||
|
<option value="CNAME">CNAME</option>
|
||||||
|
<option value="MX">MX</option>
|
||||||
|
<option value="NS">NS</option>
|
||||||
|
<option value="SRV">SRV</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="m-2 flex-1">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<label class="fieldset-legend" for="record-name">Name</label>
|
||||||
|
<input class="input validator w-full" id="record-name" bind:value={name} placeholder={namePlaceholderText} required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if recordType === 'MX'}
|
||||||
|
<div class="m-2 sm:w-20">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="fieldset-legend mr-1" for="record-priority">Priority</label>
|
||||||
|
<div class="tooltip tooltip-info mt-1" data-tip="This field is useful if you have multiple mailservers. The server with the lower priority is tried first. If it fails to receive mail, the one with the next lowest priority is tried. Set the same priority values for for load-balancing.">
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column justify-center ml-auto ">
|
||||||
|
<svg class="stroke-primary-content h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="record-priority" type="number" class="input input-bordered w-full" bind:value={priority} placeholder="10" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if recordType === 'SRV'}
|
||||||
|
<div class="m-2 sm:w-20">
|
||||||
|
<label class="form-control">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="fieldset-legend mr-1" for="record-priority">Priority</label>
|
||||||
|
<div class="tooltip tooltip-info mt-1" data-tip="This field is useful if you have multiple servers. The server with the lower priority is prioritized for traffic.">
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column justify-center ml-auto ">
|
||||||
|
<svg class="stroke-primary-content h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="record-priority" type="number" class="input validator w-full" bind:value={priority} placeholder="10" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-2 sm:w-20">
|
||||||
|
<label class="form-control">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="fieldset-legend mr-1" for="record-weight">Weight</label>
|
||||||
|
<div class="tooltip tooltip-info mt-1" data-tip="This field serves a similar purpose as the Priority, except it is looked at second. So priority takes precedence.">
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column justify-center ml-auto ">
|
||||||
|
<svg class="stroke-primary-content h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="record-weight" type="number" class="input validator w-full" bind:value={weight} placeholder="5" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-2 sm:w-20">
|
||||||
|
<label class="form-control">
|
||||||
|
<label class="fieldset-legend" for="record-port">Port</label>
|
||||||
|
<input id="record-port" type="number" class="input validator w-full" bind:value={port} placeholder="8080" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="m-2 flex-1">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<label class="fieldset-legend" for="record-value">{valueTitleText}</label>
|
||||||
|
<input class="input validator w-full" id="record-value" bind:value={value} placeholder={valuePlaceholderText} required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row">
|
||||||
|
<label class="m-2 form-control flex flex-1 flex-col">
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<label class="fieldset-legend" for="record-comment">Comment</label>
|
||||||
|
<div class="py-2 -mb-1 pl-1 text-primary-content/50">(optional)</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-center items-center">
|
||||||
|
<input class="input validator w-full mr-auto" id="record-comment" bind:value={comment} placeholder="This is my favorite DNS record!"/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="m-2 sm:w-30">
|
||||||
|
<label class="form-control flex flex-col">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<label class="fieldset-legend mr-1" for="record-ttl">TTL</label>
|
||||||
|
<div class="tooltip tooltip-info mt-1" data-tip="TTL or 'Time To Live' controls how long the record is allowed to be cached on different devices. Set a lower number if you expect your record to change value very often.">
|
||||||
|
<div role="alert" class="alert m-0 p-0 gap-2 flex flex-column justify-center ml-auto ">
|
||||||
|
<svg class="stroke-primary-content h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="number" class="input input-bordered w-full" id="record-ttl" bind:value={ttl} placeholder="300" required/>
|
||||||
|
</label>
|
||||||
|
<div class="align-right validator-hint mt-1">{error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row justify-center items-center break-all">
|
||||||
|
<RecordExplanation {recordType} {name} {value} {displayName} {displayValue} {fqdn}/>
|
||||||
|
<button class="btn btn-primary m-2 w-full sm:w-30" type="submit">Create record</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="alert alert-error text-sm">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3 h-12 flex items-center">
|
||||||
|
<ErrorPopup {errorMessage} duration={10000} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-lg m-2 pb-4 font-semibold">Manage your records</h2>
|
||||||
|
<div class="w-full max-w-6xl rounded-box border border-base-content/10 bg-base-100">
|
||||||
|
<table class="w-full table nth-2:bg-base-100 nth-4:bg-base-200">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th class="hidden lg:table-cell">TTL</th>
|
||||||
|
<th class="hidden lg:table-cell">Comment</th>
|
||||||
|
<th class="hidden lg:table-cell">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{#if loadingRecords}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center text-base-content/60">
|
||||||
|
Loading records…
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{:else if recordsError}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center text-error">
|
||||||
|
An error occured while loading your records: {error}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{:else if records.length === 0}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center text-base-content/60">
|
||||||
|
No records found. Why not create one!
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{:else}
|
||||||
|
{#each records as record, i}
|
||||||
|
<Record bind:records={records} {record} {i} {fqdn} {error}/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{"/dashboard/" + page.params.id + "/delete"}" class="btn btn-wide btn-error m-12">Delete subdomain</a>
|
||||||
|
<!-- <button class="btn btn-wide btn-error m-12" onclick={delete_subdomain_modal.showModal()}>Delete subdomain</button> -->
|
||||||
|
<!-- <dialog id="delete_subdomain_modal" class="modal">
|
||||||
|
<div class="modal-box w-11/12 max-w-5xl">
|
||||||
|
<div class="flex flex-col justify-center items-center">
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold">Are you sure you want to delete this subdomain?</h3>
|
||||||
|
<p class="py-4">All of your records for this subdomain will be deleted and other users will be able to register your domain.</p>
|
||||||
|
<p>Make sure you understand the security implications of this before proceeding.</p>
|
||||||
|
<form method="dialog" class="w-full w-fill">
|
||||||
|
<div class="flex flex-row justify-center items-center">
|
||||||
|
<button class="btn btn-wide btn-primary m-2 mr-12">Cancel</button>
|
||||||
|
<button class="btn btn-wide btn-error m-2 ml-12">Proceed</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog> -->
|
||||||
|
</div>
|
||||||
72
src/routes/dashboard/[id]/delete/+page.svelte
Normal file
72
src/routes/dashboard/[id]/delete/+page.svelte
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { page } from "$app/state";
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import { domains, type Subdomain } from "$lib/domains.svelte";
|
||||||
|
import ErrorPopup from "$lib/ErrorPopup.svelte";
|
||||||
|
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
const subdomain = $derived.by<Subdomain>(() => {
|
||||||
|
return domains.subdomainsFromId[page.params.id ?? ''];
|
||||||
|
});
|
||||||
|
|
||||||
|
const fqdn = $derived.by(() => {
|
||||||
|
return subdomain ? `${subdomain.name}.${subdomain.domain}` : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/subdomain`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({"id": page.params.id })
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
domains.subdomains = domains.subdomains.filter((sub) => sub.id !== page.params.id);
|
||||||
|
delete domains.subdomainsFromId[page.params.id ?? ""];
|
||||||
|
alert(data.msg || 'Subdomain successfully deleted');
|
||||||
|
goto("/dashboard");
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-full">
|
||||||
|
<form
|
||||||
|
class="formset bg-base-200 border-base-300 rounded-box w-xs border p-4 z-1 translate-y-2"
|
||||||
|
onsubmit={handleSubmit}>
|
||||||
|
<legend class="fieldset-legend mb-2">Are you sure you want to delete your following subdomain?</legend>
|
||||||
|
{#if domains.loadingSubdomains}
|
||||||
|
<span class="loading loading-dots"></span>
|
||||||
|
{:else}
|
||||||
|
<code class="bold">{fqdn}</code>
|
||||||
|
{/if}
|
||||||
|
<div role="alert" class="alert p-0 mt-4 mb-2 flex flex-column justify-center">
|
||||||
|
<svg class="stroke-error h-4 w-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
<span class="mr-auto text-xs text-error">All your DNS records for this domain will be deleted</span>
|
||||||
|
</div>
|
||||||
|
<div role="alert" class="alert p-0 my-2 flex flex-column justify-center">
|
||||||
|
<svg class="stroke-error h-4 w-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
<span class="mr-auto text-xs text-error">Anyone will be able to register this subdomain. Make sure you understand the security implications of this before proceeding</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-center items-center">
|
||||||
|
<button class="btn btn-primary mt-2 ml-0 mr-auto w-32" type="submit">Go back</button>
|
||||||
|
<button class="btn btn-error mt-2 mr-0 ml-auto w-32" type="submit">I'm sure</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 h-12 flex items-center">
|
||||||
|
<ErrorPopup {errorMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
139
src/routes/dashboard/register-domain/+page.svelte
Normal file
139
src/routes/dashboard/register-domain/+page.svelte
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import { domains, type Subdomain } from "$lib/domains.svelte";
|
||||||
|
import ErrorPopup from "$lib/ErrorPopup.svelte";
|
||||||
|
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
let subdomain = $state("");
|
||||||
|
|
||||||
|
let selectedDomainId = $state("");
|
||||||
|
let selectedDomainName = $state("");
|
||||||
|
$effect(() => {
|
||||||
|
if (domains.baseDomains.length > 0) {
|
||||||
|
selectedDomainId = domains.baseDomains[0].id;
|
||||||
|
selectedDomainName = domains.baseDomains[0].domain;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let available = $state<boolean | null>(null);
|
||||||
|
let loadingCheck = $state(false);
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/subdomain`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ "domain_id": selectedDomainId, "name": subdomain })
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
console.log(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSubdomain: Subdomain = {
|
||||||
|
id: data.id,
|
||||||
|
domain: selectedDomainName,
|
||||||
|
name: subdomain,
|
||||||
|
};
|
||||||
|
domains.subdomains = [...domains.subdomains, newSubdomain];
|
||||||
|
domains.subdomainsFromId[newSubdomain.id] = newSubdomain;
|
||||||
|
goto("/dashboard");
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
console.log(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAvailability() {
|
||||||
|
if (!subdomain || !selectedDomainId) {
|
||||||
|
available = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingCheck = true;
|
||||||
|
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
|
|
||||||
|
debounceTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${PUBLIC_BACKEND_API_HOST}/api/v1/subdomain/search/${selectedDomainId}/${subdomain}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("Lookup failed");
|
||||||
|
|
||||||
|
available = data.available === true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
available = null;
|
||||||
|
} finally {
|
||||||
|
loadingCheck = false;
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
checkAvailability();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-full">
|
||||||
|
<form
|
||||||
|
class="formset bg-base-200 border-base-300 rounded-box w-xs border p-4 z-1 translate-y-2"
|
||||||
|
onsubmit={handleSubmit}>
|
||||||
|
<legend class="fieldset-legend">Register a new domain</legend>
|
||||||
|
<div class="form-control my-4 mt-6">
|
||||||
|
<label class="text-primary-content" for="subdomain">
|
||||||
|
<span class="label-text">Subdomain</span>
|
||||||
|
</label>
|
||||||
|
<div class="join w-full">
|
||||||
|
<input type="text" placeholder="example" class="input input-bordered join-item w-full" bind:value={subdomain}/>
|
||||||
|
{#if !domains.loadingBaseDomains}
|
||||||
|
<select
|
||||||
|
class="select select-bordered join-item"
|
||||||
|
bind:value={selectedDomainId}
|
||||||
|
onchange={(e) => {
|
||||||
|
const target = e.target as HTMLSelectElement;
|
||||||
|
const d = domains.baseDomains.find(d => d.id === target.value);
|
||||||
|
selectedDomainName = d?.domain ?? "";
|
||||||
|
}}>
|
||||||
|
{#each domains.baseDomains as d}
|
||||||
|
<option value={d.id}>{d.domain}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="mt-1 text-sm font-medium"
|
||||||
|
class:text-success={available}
|
||||||
|
class:text-error={!available}
|
||||||
|
>
|
||||||
|
{#if available !== null && !loadingCheck}
|
||||||
|
{subdomain}.{selectedDomainName}
|
||||||
|
{available ? " is available" : " is unavailable"}
|
||||||
|
{:else}
|
||||||
|
 
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-full" type="submit" disabled={!available || loadingCheck}>{loadingCheck ? "Checking..." : "Register"}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3 h-12 flex items-center">
|
||||||
|
<ErrorPopup {errorMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
54
src/routes/delete-account/+page.svelte
Normal file
54
src/routes/delete-account/+page.svelte
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import ErrorPopup from "$lib/ErrorPopup.svelte";
|
||||||
|
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/user/me`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(data.msg || 'Account successfully deleted');
|
||||||
|
goto("/register");
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-full">
|
||||||
|
<form
|
||||||
|
class="formset bg-base-200 border-base-300 rounded-box w-xs border p-4 z-1 translate-y-2"
|
||||||
|
onsubmit={handleSubmit}>
|
||||||
|
<legend class="fieldset-legend">Are you sure you want to delete your account?</legend>
|
||||||
|
<button class="btn btn-error w-full mt-2 mb-3" type="submit">Delete my account</button>
|
||||||
|
<div role="alert" class="alert p-0 mb-2 flex flex-column justify-center">
|
||||||
|
<svg class="stroke-error h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
<span class="mr-auto text-xs text-error">This action is irreversible</span>
|
||||||
|
</div>
|
||||||
|
<div role="alert" class="alert p-0 my-2 flex flex-column justify-center">
|
||||||
|
<svg class="stroke-error h-4 w-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
<span class="mr-auto text-xs text-error">All your DNS records will be deleted</span>
|
||||||
|
</div>
|
||||||
|
<div role="alert" class="alert p-0 my-2 flex flex-column justify-center">
|
||||||
|
<svg class="stroke-error h-4 w-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
<span class="mr-auto text-xs text-error">Anyone will be able to register your currently owned subdomains. Make sure you understand the security implications of this</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 h-12 flex items-center">
|
||||||
|
<ErrorPopup {errorMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
150
src/routes/faq/+page.svelte
Normal file
150
src/routes/faq/+page.svelte
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
<div class="flex flex-col items-center min-h-full">
|
||||||
|
<div class="w-4xl">
|
||||||
|
|
||||||
|
<div class="hero bg-base-200">
|
||||||
|
<div class="hero-content flex-col lg:flex-row-reverse">
|
||||||
|
<div class="mockup-code w-110">
|
||||||
|
<pre data-prefix="$"><code>Question?</code></pre>
|
||||||
|
<pre data-prefix=">" class="text-warning"><code>Answering...</code></pre>
|
||||||
|
<pre data-prefix=">" class="text-success"><code>Done!</code></pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold">Frequently asked questions</h1>
|
||||||
|
<div class="py-6">
|
||||||
|
Find some of HexName's most frequently asked questions and answers about our services, security, getting started, and support.
|
||||||
|
</div>
|
||||||
|
<a href="/register" class="btn btn-primary w-40">Get started</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
<h2 class="legend text-xl font-bold p-4 m-4">Questions about our services</h2>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">What is HexName?</div>
|
||||||
|
<div class="collapse-content text-sm">HexName is a free DNS service that allows you to register and manage subdomains under domains we own, such as <code>example.hexname.com</code>, <code>example.loves-beer.com</code> and others.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">Where can I learn more about using HexName?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
The easiest way to learn how to use HexName is to <a href="/register" class="link link-secondary">sign up</a>, which takes less than 1 minute.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">Do you offer Dynamic DNS services?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
Yes! We provide a DDNS service for A and AAAA records, allowing you to update your DNS records automatically whenever your IP address changes.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">Are there going to be any costs in the future?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
Never. We promise to never put the basic features provided behind a paywall, and to keep the necessary features always 100% free.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">Is this service really free?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
Yes. The DNS and DDNS services are provided free of charge, with no usage fees.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">What DNS record types do you support?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
You can create and manage A, AAAA, TXT, CNAME, MX, NS, and SRV records for your registered subdomains.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">What domains does HexName provide for registration?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
As of now, we own the following domains: <code>hexname.com</code>, <code>loves-beer.com</code>, <code>dickdns.org</code>, and they can be used to register available subdomains.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">What does DDNS mean?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
DDNS stands for Dynamic DNS and is a service that lets you update a DNS record (usually an A/AAAA record) to point to your desired IP address.
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
This is mostly useful for individuals who would like to expose services run at their home to the internet and have a memorable domain instead of having to use the IP address.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
<h2 class="legend text-xl font-bold p-4 m-4">Questions about your account</h2>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">I forgot my password. What should I do?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
Click <a class="link link-secondary" href="/forgot-password">here</a> or on "Forgot password" on the login page and follow the instructions sent to your email.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">How do I delete my account?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
Click <a class="link link-secondary" href="/delete-account">here</a> or the profile icon in the top right corner, then "Delete account", and you'll be prompted to the confirmation page.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">Can I register multiple domains?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
Yes. You may register up to 20 domains and manage any subdomain under them, such as <code>mail.example.hexname.com</code> and <code>email.example.hexname.com</code>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
<h2 class="legend text-xl font-bold p-4 m-4">Miscellaneous questions</h2>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 border border-base-300">
|
||||||
|
<input type="radio"/>
|
||||||
|
<div class="collapse-title font-semibold">How does HexName make money?</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="p-2">
|
||||||
|
I don't make a cent lol
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
52
src/routes/forgot-password/+page.svelte
Normal file
52
src/routes/forgot-password/+page.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import ErrorPopup from "$lib/ErrorPopup.svelte";
|
||||||
|
|
||||||
|
let email = '';
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/user/request-password-reset`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email })
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(data.msg || 'Success!'); // TODO
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-full">
|
||||||
|
<form
|
||||||
|
class="formset bg-base-200 border-base-300 rounded-box w-xs border p-4 z-1 translate-y-2"
|
||||||
|
onsubmit={handleSubmit}>
|
||||||
|
<legend class="fieldset-legend">Forgot your password?</legend>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="text-primary-content" for="email">Email</label>
|
||||||
|
<input class="input validator" id="email" name="email" autocomplete="email" type="email" bind:value={email} placeholder="you@example.com" required/>
|
||||||
|
<div class="align-right validator-hint mt-1">Enter valid email address</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-full mt-2 mb-3" type="submit">Submit</button>
|
||||||
|
<div role="alert" class="alert p-0 flex flex-column justify-center">
|
||||||
|
<svg class="stroke-info h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
<span class="mr-auto text-xs text-info">An email will only be sent if the account exists</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 h-12 flex items-center">
|
||||||
|
<ErrorPopup {errorMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
64
src/routes/layout.css
Normal file
64
src/routes/layout.css
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
@plugin "daisyui";
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "dark";
|
||||||
|
default: true;
|
||||||
|
prefersdark: true;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(25.33% 0.016 252.42);
|
||||||
|
--color-base-200: oklch(23.26% 0.014 253.1);
|
||||||
|
--color-base-300: oklch(21.15% 0.012 254.09);
|
||||||
|
--color-base-content: oklch(92% 0.013 255.508);
|
||||||
|
--color-primary: oklch(48% 0.243 264.376);
|
||||||
|
--color-primary-content: oklch(93% 0.032 255.585);
|
||||||
|
--color-secondary: oklch(65% 0.241 354.308);
|
||||||
|
--color-secondary-content: oklch(94% 0.028 342.258);
|
||||||
|
--color-accent: oklch(77% 0.152 181.912);
|
||||||
|
--color-accent-content: oklch(38% 0.063 188.416);
|
||||||
|
--color-neutral: oklch(14% 0.005 285.823);
|
||||||
|
--color-neutral-content: oklch(92% 0.004 286.32);
|
||||||
|
--color-info: oklch(74% 0.16 232.661);
|
||||||
|
--color-info-content: oklch(29% 0.066 243.157);
|
||||||
|
--color-success: oklch(79% 0.209 151.711);
|
||||||
|
--color-success-content: oklch(39% 0.095 152.535);
|
||||||
|
--color-warning: oklch(90% 0.182 98.111);
|
||||||
|
--color-warning-content: oklch(42% 0.095 57.708);
|
||||||
|
--color-error: oklch(64% 0.246 16.439);
|
||||||
|
--color-error-content: oklch(27% 0.105 12.094);
|
||||||
|
--radius-selector: 0.5rem;
|
||||||
|
--radius-field: 0.25rem;
|
||||||
|
--radius-box: 0.5rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--animate-fadeInDown: fadeInDown 1s ease-in;
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
--animate-fadeOutDown: fadeOutDown 1s ease-out;
|
||||||
|
@keyframes fadeOutDown {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/routes/login/+page.svelte
Normal file
64
src/routes/login/+page.svelte
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import ErrorPopup from "$lib/ErrorPopup.svelte";
|
||||||
|
import { auth } from "$lib/auth.svelte";
|
||||||
|
import { getSubdomains } from '$lib/domains.svelte';
|
||||||
|
|
||||||
|
let email = '';
|
||||||
|
let password = '';
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/user/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
console.log(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubdomains();
|
||||||
|
auth.isAuthenticated = true;
|
||||||
|
goto("/dashboard");
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
console.log(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-full">
|
||||||
|
<form
|
||||||
|
class="formset bg-base-200 border-base-300 rounded-box w-xs border p-4 z-1 translate-y-2"
|
||||||
|
onsubmit={handleSubmit}>
|
||||||
|
<legend class="fieldset-legend">Log in to your account</legend>
|
||||||
|
<div class="mt-4 mb-2">
|
||||||
|
<label class="text-base-content" for="email">Email</label>
|
||||||
|
<input class="input validator" id="email" name="email" autocomplete="email" type="email" bind:value={email} placeholder="you@example.com" required/>
|
||||||
|
<div class="align-right validator-hint mt-1">Enter valid email address</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 mb-2">
|
||||||
|
<label class="text-base-content" for="password">Password</label>
|
||||||
|
<input class="input validator" minlength="12" title="The password must be at least 12 characters long" id="password" name="password" autocomplete="current-password" type="password" bind:value={password} placeholder="****************" required/>
|
||||||
|
<p class="validator-hint">The password must be at least 12 characters long</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-between mt-2">
|
||||||
|
<button class="btn btn-primary w-35" type="submit">Login</button>
|
||||||
|
<a class="mt-auto mb-auto text-xs text-primary-content" type="submit" href="/forgot-password">Forgot password</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 h-12 flex items-center">
|
||||||
|
<ErrorPopup {errorMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
27
src/routes/logout/+page.svelte
Normal file
27
src/routes/logout/+page.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import { auth } from "$lib/auth.svelte";
|
||||||
|
import { clearSubdomains, getSubdomains } from '$lib/domains.svelte';
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/user/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let errorMessage = data?.msg || "Something went wrong";
|
||||||
|
console.log(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear subdomains
|
||||||
|
clearSubdomains();
|
||||||
|
auth.isAuthenticated = false;
|
||||||
|
goto("/redirect/login");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
1
src/routes/redirect/[url]/+layout.ts
Normal file
1
src/routes/redirect/[url]/+layout.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const ssr = false;
|
||||||
15
src/routes/redirect/[url]/+page.svelte
Normal file
15
src/routes/redirect/[url]/+page.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
|
let target = page.params.url;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
goto("/" + target);
|
||||||
|
}, 1500);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-full">
|
||||||
|
<h2 class="text-4xl text-primary-content">Redirecting you to the {target} page</h2>
|
||||||
|
<span class="loading loading-dots loading-lg translate-y-3 ml-1"></span>
|
||||||
|
</div>
|
||||||
79
src/routes/register/+page.svelte
Normal file
79
src/routes/register/+page.svelte
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import ErrorPopup from "$lib/ErrorPopup.svelte";
|
||||||
|
|
||||||
|
let email = '';
|
||||||
|
let password = '';
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/user/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
console.log(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(data.msg || 'Success!'); // TODO
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
console.log(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- <div class="container flex flex-col items-center justify-center mx-auto min-h-screen w-full h-full py-8"> -->
|
||||||
|
<!-- <div class="container flex flex-col items-center justify-center"> -->
|
||||||
|
<!-- <div class="absolute h-2/5 pt-auto"> -->
|
||||||
|
<!-- <div class="container flex flex-col items-center justify-center mx-auto min-h-screen py-8">
|
||||||
|
<form
|
||||||
|
class="formset bg-base-200 border-base-300 rounded-box w-xs border p-4 z-1"
|
||||||
|
onsubmit={handleSubmit}>
|
||||||
|
<legend class="fieldset-legend">Create your account</legend>
|
||||||
|
<div class="mt-4 mb-4">
|
||||||
|
<label class="label" for="email">Email</label>
|
||||||
|
<input class="input" id="email" name="email" autocomplete="email" type="email" bind:value={email} placeholder="you@example.com" required/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 mb-4">
|
||||||
|
<label class="label" for="password">Password</label>
|
||||||
|
<input class="input" id="password" name="password" autocomplete="new-password" type="password" bind:value={password} placeholder="****************" required/>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-full mt-2" type="submit">Register</button>
|
||||||
|
</form>
|
||||||
|
<div class="flex-wrap">
|
||||||
|
<ErrorPopup {errorMessage}/>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-full">
|
||||||
|
<form
|
||||||
|
class="formset bg-base-200 border-base-300 rounded-box w-xs border p-4 z-1 translate-y-2"
|
||||||
|
onsubmit={handleSubmit}>
|
||||||
|
<legend class="fieldset-legend">Create your account</legend>
|
||||||
|
<div class="mt-4 mb-2">
|
||||||
|
<label class="text-primary-content" for="email">Email</label>
|
||||||
|
<input class="input validator" id="email" name="email" autocomplete="email" type="email" bind:value={email} placeholder="you@example.com" required/>
|
||||||
|
<div class="align-right validator-hint mt-1">Enter valid email address</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 mb-2">
|
||||||
|
<label class="text-primary-content" for="password">Password</label>
|
||||||
|
<input class="input validator" minlength="12" title="The password must be at least 12 characters long" id="password" name="password" autocomplete="current-password" type="password" bind:value={password} placeholder="****************" required/>
|
||||||
|
<p class="validator-hint">The password must be at least 12 characters long</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-full" type="submit">Register</button>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 h-12 flex items-center">
|
||||||
|
<ErrorPopup {errorMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
48
src/routes/verify-email/[token]/+page.svelte
Normal file
48
src/routes/verify-email/[token]/+page.svelte
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from "$app/state";
|
||||||
|
import { PUBLIC_BACKEND_API_HOST } from "$env/static/public";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let token = page.params.token;
|
||||||
|
let errorMessage: string | undefined = $state(undefined);
|
||||||
|
let success: boolean | undefined = $state(undefined);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${PUBLIC_BACKEND_API_HOST}/api/v1/user/verify-email`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ token })
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
errorMessage = data?.msg || "Something went wrong";
|
||||||
|
success = false;
|
||||||
|
} else {
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage = err?.msg || "Network error";
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if success === undefined}
|
||||||
|
<div class="container flex items-center justify-center mx-auto min-h-screen py-8">
|
||||||
|
<h2 class="text-4xl text-primary-content">Verifying your email</h2>
|
||||||
|
<span class="loading loading-dots loading-lg translate-y-3 ml-1"></span>
|
||||||
|
</div>
|
||||||
|
{:else if success}
|
||||||
|
<div class="container flex flex-col items-center justify-center mx-auto min-h-screen py-8">
|
||||||
|
<h2 class="text-2xl text-primary-content">Email successfully verified!</h2>
|
||||||
|
<h3 class="text-xl text-primary-content">You may log in now.</h3>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="container flex flex-col items-center justify-center mx-auto min-h-screen py-8">
|
||||||
|
<h2 class="text-2xl text-primary-content">Failed to verify your email:</h2>
|
||||||
|
<h3 class="text-xl text-primary-content">{errorMessage}</h3>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
12
start_backend.sh
Executable file
12
start_backend.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd ~/Documents/Prog/dns
|
||||||
|
sudo caddy run &
|
||||||
|
|
||||||
|
cd ~/Documents/Prog/dns/dns-server
|
||||||
|
sudo systemctl start docker
|
||||||
|
sudo docker-compose up -d &
|
||||||
|
|
||||||
|
cd ~/Documents/Prog/dns/dns-backend
|
||||||
|
cargo run
|
||||||
3
static/robots.txt
Normal file
3
static/robots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# allow crawling everything by default
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
12
svelte.config.js
Normal file
12
svelte.config.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import adapter from "@sveltejs/adapter-node";
|
||||||
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: { adapter: adapter() },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rewriteRelativeImportExtensions": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||||
|
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue