summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
Diffstat (limited to 'public')
-rw-r--r--public/css/home.css40
-rw-r--r--public/css/login.css18
-rw-r--r--public/css/main.css119
-rw-r--r--public/css/record.css67
-rw-r--r--public/domain.html21
-rw-r--r--public/fonts/helvetica-bold.ttfbin0 -> 308628 bytes
-rw-r--r--public/fonts/helvetica.ttfbin0 -> 317968 bytes
-rw-r--r--public/fonts/overpass-bold-italic.otfbin0 -> 71760 bytes
-rw-r--r--public/fonts/overpass-bold.otfbin0 -> 68828 bytes
-rw-r--r--public/home.html21
-rw-r--r--public/js/api.js51
-rw-r--r--public/js/components.js12
-rw-r--r--public/js/domain.js95
-rw-r--r--public/js/home.js91
-rw-r--r--public/js/login.js44
-rw-r--r--public/js/main.js136
-rw-r--r--public/login.html21
-rw-r--r--public/robots.txt9
18 files changed, 745 insertions, 0 deletions
diff --git a/public/css/home.css b/public/css/home.css
new file mode 100644
index 0000000..2548a28
--- /dev/null
+++ b/public/css/home.css
@@ -0,0 +1,40 @@
+span {
+ margin-top: 5rem;
+ margin-bottom: 1rem;
+ width: 45rem;
+ font-size: 2em;
+}
+
+#new {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ padding-top: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: solid 1px var(--gray);
+}
+
+#new input, .block {
+ border-radius: 1rem 0 0 1rem;
+ width: 40rem;
+}
+
+.block {
+ width: 33em;
+}
+
+#new button {
+ border-radius: 0 1rem 1rem 0;
+}
+
+.domain {
+ margin-top: 2rem;
+}
+
+.domain .delete {
+ border-radius: 0 1rem 1rem 0;
+}
+
+.domain .edit {
+ border-radius: 0;
+} \ No newline at end of file
diff --git a/public/css/login.css b/public/css/login.css
new file mode 100644
index 0000000..2be7c13
--- /dev/null
+++ b/public/css/login.css
@@ -0,0 +1,18 @@
+#login {
+ margin-top: 20em;
+}
+
+#logo {
+ font-size: 6em;
+ font-weight: 750;
+ font-family: bold;
+ margin-bottom: 2rem;
+}
+
+form {
+ width: 30rem;
+}
+
+form input {
+ width: 100%;
+} \ No newline at end of file
diff --git a/public/css/main.css b/public/css/main.css
new file mode 100644
index 0000000..971e2bb
--- /dev/null
+++ b/public/css/main.css
@@ -0,0 +1,119 @@
+:root {
+ --dark: #222428;
+ --dark-alternate: #2b2e36;
+ --header: #1e1e22;
+
+ --accent: #8849f5;
+ --accent-alternate: #6829d5;
+ --gray: #2f2f3f;
+ --main: #ffffff;
+ --main-alternate: #cccccc;
+}
+
+* {
+ padding: 0;
+ margin: 0;
+}
+
+@font-face {
+ font-family: main;
+ src: url("../fonts/helvetica.ttf") format("truetype");
+ font-display: swap;
+}
+
+@font-face {
+ font-family: bold;
+ src: url("../fonts/overpass-bold.otf") format("opentype");
+ font-display: swap;
+}
+
+@font-face {
+ font-family: bold-italic;
+ src: url("../fonts/overpass-bold-italic.otf") format("opentype");
+ font-display: swap;
+}
+
+html {
+ background-color: var(--dark);
+ font-family: main;
+ color: var(--main);
+ width: 100%;
+ height: 100%;
+}
+
+body {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.accent {
+ color: var(--accent);
+}
+
+.fill {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+input, button, .block {
+ all: unset;
+ display: inline-block;
+ font: main;
+ background-color: var(--dark-alternate);
+ font-size: 1rem;
+ padding: 1rem;
+ border-radius: 1rem;
+ margin-bottom: 20px;
+}
+
+button {
+ background-color: var(--accent);
+ width: 5em;
+ text-align: center;
+}
+
+button:hover {
+ cursor: pointer;
+ background-color: var(--accent-alternate);
+}
+
+.delete {
+ background-color: #f54842;
+}
+
+.delete:hover {
+ cursor: pointer;
+ background-color: #d52822;
+}
+
+form {
+ display: flex;
+ flex-direction: column;
+}
+
+#header {
+ width: calc(100% - 4rem);
+ background-color: var(--header);
+ border-bottom: solid 1px var(--gray);
+ padding: 1rem;
+ padding-left: 3rem;
+}
+
+#logo {
+ font-size: 2em;
+ font-weight: 500;
+ font-family: bold;
+}
+
+#title {
+ font-size: 2em;
+ font-weight: 300;
+ font-family: sans-serif;
+ padding-left: 1em;
+} \ No newline at end of file
diff --git a/public/css/record.css b/public/css/record.css
new file mode 100644
index 0000000..3dc257a
--- /dev/null
+++ b/public/css/record.css
@@ -0,0 +1,67 @@
+#buttons {
+ margin-top: 2rem;
+ width: 50rem;
+}
+
+#buttons button {
+ margin: 0;
+ margin-right: 2rem;
+ border-radius: 10px;
+ width: auto;
+ padding: .75rem 1rem;
+}
+
+.record {
+ width: 50rem;
+ background-color: var(--header);
+ padding: 1rem;
+ margin-top: 2rem;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.header span {
+ font-family: bold;
+}
+
+.header button {
+ margin: 0;
+ margin-left: 2rem;
+ padding: .5rem 1rem;
+ width: auto;
+ border-radius: 5px;
+}
+
+.type {
+ margin-right: 1rem;
+ background-color: var(--accent);
+ padding: .25rem .5rem;
+ border-radius: 5px;
+}
+
+.domain {
+ color: var(--main-alternate);
+ flex-grow: 1;
+}
+
+.properties {
+ display: flex;
+ flex-direction: column;
+}
+
+.poperty {
+ display: flex;
+ flex-direction: row;
+ border-bottom: solid 1px var(--gray);
+ margin-top: 1rem;
+}
+
+.key {
+ font-family: bold;
+ width: 5rem;
+}
+
diff --git a/public/domain.html b/public/domain.html
new file mode 100644
index 0000000..ba22eaf
--- /dev/null
+++ b/public/domain.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Wrapper - Records</title>
+
+ <meta name="author" content="Tyler Murphy">
+ <meta name="description" content="wrapper records">
+
+ <meta property="og:title" content="wrapper">
+ <meta property="og:description" content="wrapper records">
+
+ <link rel="stylesheet" href="/css/main.css">
+ <link rel="stylesheet" href="/css/record.css">
+
+ <script type="module" src="/js/domain.js"></script>
+</head>
+<body>
+</body>
+</html> \ No newline at end of file
diff --git a/public/fonts/helvetica-bold.ttf b/public/fonts/helvetica-bold.ttf
new file mode 100644
index 0000000..332b66c
--- /dev/null
+++ b/public/fonts/helvetica-bold.ttf
Binary files differ
diff --git a/public/fonts/helvetica.ttf b/public/fonts/helvetica.ttf
new file mode 100644
index 0000000..718f22d
--- /dev/null
+++ b/public/fonts/helvetica.ttf
Binary files differ
diff --git a/public/fonts/overpass-bold-italic.otf b/public/fonts/overpass-bold-italic.otf
new file mode 100644
index 0000000..f4929ae
--- /dev/null
+++ b/public/fonts/overpass-bold-italic.otf
Binary files differ
diff --git a/public/fonts/overpass-bold.otf b/public/fonts/overpass-bold.otf
new file mode 100644
index 0000000..962a5d7
--- /dev/null
+++ b/public/fonts/overpass-bold.otf
Binary files differ
diff --git a/public/home.html b/public/home.html
new file mode 100644
index 0000000..9efb21e
--- /dev/null
+++ b/public/home.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Wrapper - Domains</title>
+
+ <meta name="author" content="Tyler Murphy">
+ <meta name="description" content="wrapper domains">
+
+ <meta property="og:title" content="wrapper">
+ <meta property="og:description" content="wrapper domains">
+
+ <link rel="stylesheet" href="/css/main.css">
+ <link rel="stylesheet" href="/css/home.css">
+
+ <script type="module" src="/js/home.js"></script>
+</head>
+<body>
+</body>
+</html> \ No newline at end of file
diff --git a/public/js/api.js b/public/js/api.js
new file mode 100644
index 0000000..7a29d65
--- /dev/null
+++ b/public/js/api.js
@@ -0,0 +1,51 @@
+const endpoint = '/api'
+
+const request = async (url, method, body) => {
+
+ let response;
+
+ if (method == 'GET') {
+ response = await fetch(endpoint + url, {
+ method,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+ } else {
+ response = await fetch(endpoint + url, {
+ method,
+ body: JSON.stringify(body),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+ }
+
+ if (response.status == 401) {
+ location.href = '/login'
+ }
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.indexOf("application/json") !== -1) {
+ const json = await response.json()
+ return { status: response.status, msg: json.msg, json }
+ } else {
+ const msg = await response.text();
+ return { status: response.status, msg }
+ }
+}
+
+export const login = async (user, pass) => {
+ return await request('/login', 'POST', {user, pass})
+}
+
+export const domains = async () => {
+ return await request('/domains', 'GET')
+}
+
+export const del_domain = async (domain) => {
+ return await request('/domains', 'DELETE', {domain})
+}
+
+export const records = async (domain) => {
+ return await request(`/records?domain=${domain}`, 'GET')
+} \ No newline at end of file
diff --git a/public/js/components.js b/public/js/components.js
new file mode 100644
index 0000000..00def5f
--- /dev/null
+++ b/public/js/components.js
@@ -0,0 +1,12 @@
+import { div, parse, span } from './main.js';
+
+export function header(title) {
+ return div({id: 'header'},
+ span({id: 'logo', class: 'accent'},
+ parse("Wrapper")
+ ),
+ span({id: 'title'},
+ parse(title)
+ ),
+ )
+} \ No newline at end of file
diff --git a/public/js/domain.js b/public/js/domain.js
new file mode 100644
index 0000000..36af422
--- /dev/null
+++ b/public/js/domain.js
@@ -0,0 +1,95 @@
+import { del_domain, domains, records } from './api.js'
+import { header } from './components.js'
+import { body, parse, div, input, button, span, is_domain } from './main.js';
+
+function render(domain, records) {
+
+ let divs = []
+ for (const record of records) {
+ divs.push(gen_record(record))
+ }
+
+ document.body.replaceWith(
+ body({},
+ header(domain),
+ div({id: 'buttons'},
+ button({onclick: (event) => {
+ location.href = '/home'
+ }}, parse("Home")),
+ button({}, parse("New Record")),
+ ),
+ ...divs
+ )
+ )
+}
+
+function gen_record(record) {
+ let domain = record.domain
+ let prefix = record.prefix
+
+ if (prefix.length > 0) {
+ prefix = prefix + '.'
+ }
+
+ let type = Object.keys(record.record)[0]
+ let data = record.record[type]
+
+ let divs = []
+ for (const key in data) {
+ let disp_key;
+ if (key == 'ttl') {
+ disp_key = 'TTL'
+ } else {
+ disp_key = upper(key)
+ }
+ divs.push(
+ div({class: 'poperty'},
+ div({class: 'key'}, parse(disp_key)),
+ div({class: 'value'}, parse(data[key])),
+ )
+ )
+ }
+
+ return div({class: 'record'},
+ div({class: 'header'},
+ span({class: 'type'}, parse(type)),
+ span({class: 'prefix'}, parse(prefix)),
+ span({class: 'domain'}, parse(domain)),
+ button({}, parse("Edit")),
+ button({class: 'delete'}, parse("Delete"))
+ ),
+ div({class: 'properties'},
+ ...divs
+ )
+ )
+}
+
+function upper(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+async function init() {
+
+ const params = new Proxy(new URLSearchParams(window.location.search), {
+ get: (searchParams, prop) => searchParams.get(prop),
+ });
+
+ let domain = params.domain;
+
+ if (!is_domain(domain)) {
+ location.href = '/home'
+ return
+ }
+
+ let res = await records(domain);
+
+ if (res.status !== 200) {
+ alert(res.msg)
+ return
+ }
+
+ render(domain, res.json)
+
+}
+
+init() \ No newline at end of file
diff --git a/public/js/home.js b/public/js/home.js
new file mode 100644
index 0000000..f615632
--- /dev/null
+++ b/public/js/home.js
@@ -0,0 +1,91 @@
+import { del_domain, domains } from './api.js'
+import { header } from './components.js'
+import { body, parse, div, input, button, span, is_domain } from './main.js';
+
+function render(domains) {
+ document.body.replaceWith(
+ body({},
+ header('domains'),
+ div({id: 'new'},
+ input({
+ type: 'text',
+ name: 'domain',
+ id: 'domain',
+ placeholder: 'Type domain name to create new records',
+ autocomplete: "off",
+ }),
+ button({onclick: () => {
+ let domain = document.getElementById('domain').value
+
+ if (!is_domain(domain)) {
+ alert("Invalid domain")
+ return
+ }
+
+ location.href = '/domain?domain='+domain
+ }},
+ parse("Create")
+ )
+ ),
+ ...domain(domains)
+ )
+ )
+}
+
+function domain(domains) {
+ let divs = []
+ for (const domain of domains) {
+ divs.push(
+ div({class: 'domain'},
+ div({class: 'block'},
+ parse(domain)
+ ),
+ button({class: 'edit', onclick: (event) => {
+ console.log(event.target.parentElement)
+ let domain = event
+ .target
+ .parentElement
+ .getElementsByClassName('block')[0]
+ .innerText
+
+ if (!is_domain(domain)) {
+ alert("Invalid domain")
+ return
+ }
+
+ location.href = '/domain?domain='+domain
+ }},
+ parse("Edit")
+ ),
+ button({class: 'delete', onclick: async () => {
+ let res = await del_domain(domain)
+
+ if (res.status != 204) {
+ alert(res.msg)
+ return
+ }
+
+ location.reload()
+ }},
+ parse("Delete")
+ )
+ )
+ )
+ }
+ return divs
+}
+
+async function init() {
+
+ let res = await domains();
+
+ if (res.status !== 200) {
+ alert(res.msg)
+ return
+ }
+
+ render(res.json)
+
+}
+
+init() \ No newline at end of file
diff --git a/public/js/login.js b/public/js/login.js
new file mode 100644
index 0000000..3bd64ad
--- /dev/null
+++ b/public/js/login.js
@@ -0,0 +1,44 @@
+import { body, div, form, input, p, parse, span} from './main.js'
+import { login } from './api.js'
+
+function render() {
+ document.body.replaceWith(
+ body({},
+ div({id: 'login', class: 'fill'},
+ span({id: 'logo'},
+ span({class: 'accent'}, parse('Wrapper'))
+ ),
+ form({autocomplete: "off"},
+ input({
+ type: 'text',
+ name: 'user',
+ id: 'user',
+ placeholder: 'Username',
+ autofocus: 1
+ }),
+ input({
+ type: 'password',
+ name: 'pass',
+ id: 'pass',
+ placeholder: 'Password',
+ onkeydown: async (event) => {
+ if (event.key == 'Enter') {
+ event.preventDefault()
+ let user = document.getElementById('user').value
+ let pass = document.getElementById('pass').value
+
+ let res = await login(user, pass)
+
+ if (res.status === 200) {
+ location.href = '/home'
+ }
+ }
+ }
+ })
+ )
+ )
+ )
+ )
+}
+
+render()
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..615b3d6
--- /dev/null
+++ b/public/js/main.js
@@ -0,0 +1,136 @@
+function createElement(name, attrs, ...children) {
+ const el = document.createElement(name);
+
+ for (const attr in attrs) {
+ if(attr.startsWith("on")) {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr])
+ }
+ }
+
+ for (const child of children) {
+ if (child == null) {
+ continue
+ }
+ el.appendChild(child)
+ }
+
+ return el
+}
+
+export function createElementNS(name, attrs, ...children) {
+ var svgns = "http://www.w3.org/2000/svg";
+ var el = document.createElementNS(svgns, name);
+
+ for (const attr in attrs) {
+ if(attr.startsWith("on")) {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr])
+ }
+ }
+
+ for (const child of children) {
+ if (child == null) {
+ continue
+ }
+ el.appendChild(child)
+ }
+
+ return el
+}
+
+export function p(attrs, ...children) {
+ return createElement("p", attrs, ...children)
+}
+
+export function span(attrs, ...children) {
+ return createElement("span", attrs, ...children)
+}
+
+export function div(attrs, ...children) {
+ return createElement("div", attrs, ...children)
+}
+
+export function a(attrs, ...children) {
+ return createElement("a", attrs, ...children)
+}
+
+export function i(attrs, ...children) {
+ return createElement("i", attrs, ...children)
+}
+
+export function form(attrs, ...children) {
+ return createElement("form", attrs, ...children)
+}
+
+export function img(alt, attrs, ...children) {
+ attrs['onerror'] = (event) => event.target.remove()
+ attrs['alt'] = alt
+ return createElement("img", attrs, ...children)
+}
+
+export function input(attrs, ...children) {
+ return createElement("input", attrs, ...children)
+}
+
+export function button(attrs, ...children) {
+ return createElement("button", attrs, ...children)
+}
+
+export function path(attrs, ...children) {
+ return createElementNS("path", attrs, ...children)
+}
+
+export function svg(attrs, ...children) {
+ return createElementNS("svg", attrs, ...children)
+}
+
+export function body(attrs, ...children) {
+ return createElement("body", attrs, ...children)
+}
+
+export function textarea(attrs, ...children) {
+ return createElement("textarea", attrs, ...children)
+}
+
+export function parse(input) {
+ const pattern = /^[ a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*$/;
+
+ input = input + '';
+
+ if (!pattern.test(input)) {
+ return null;
+ }
+
+ const sanitized = input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+ return document.createRange().createContextualFragment(sanitized);
+}
+
+export function is_domain(domain) {
+ domain = domain.toLowerCase()
+
+ const pattern = /^[a-z0-9_\-.]*$/;
+ if (!pattern.test(domain)) {
+ return false
+ }
+
+ let parts = domain.split('.').reverse()
+ for (const part of parts) {
+ if (part.length < 1) {
+ return false
+ }
+ }
+
+ if (parts.length < 2 || parts[0].length < 2) {
+ return false
+ }
+
+ const tld_pattern = /^[a-z]*$/;
+ if (!tld_pattern.test(parts[0])) {
+ return false
+ }
+
+ return true
+} \ No newline at end of file
diff --git a/public/login.html b/public/login.html
new file mode 100644
index 0000000..b03324b
--- /dev/null
+++ b/public/login.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Wrapper - Login</title>
+
+ <meta name="author" content="Tyler Murphy">
+ <meta name="description" content="wrapper dns login">
+
+ <meta property="og:title" content="wrapper">
+ <meta property="og:description" content="wrapper dns login">
+
+ <link rel="stylesheet" href="/css/main.css">
+ <link rel="stylesheet" href="/css/login.css">
+
+ <script type="module" src="/js/login.js"></script>
+</head>
+<body>
+</body>
+</html> \ No newline at end of file
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..b1b4ec3
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,9 @@
+User-agent: Googlebot
+Disallow: /api
+
+User-agent: Googlebot
+User-agent: AdsBot-Google
+Disallow: /api
+
+User-agent: *
+Disallow: /api \ No newline at end of file