diff options
Diffstat (limited to 'public')
-rw-r--r-- | public/css/home.css | 40 | ||||
-rw-r--r-- | public/css/login.css | 18 | ||||
-rw-r--r-- | public/css/main.css | 119 | ||||
-rw-r--r-- | public/css/record.css | 67 | ||||
-rw-r--r-- | public/domain.html | 21 | ||||
-rw-r--r-- | public/fonts/helvetica-bold.ttf | bin | 0 -> 308628 bytes | |||
-rw-r--r-- | public/fonts/helvetica.ttf | bin | 0 -> 317968 bytes | |||
-rw-r--r-- | public/fonts/overpass-bold-italic.otf | bin | 0 -> 71760 bytes | |||
-rw-r--r-- | public/fonts/overpass-bold.otf | bin | 0 -> 68828 bytes | |||
-rw-r--r-- | public/home.html | 21 | ||||
-rw-r--r-- | public/js/api.js | 51 | ||||
-rw-r--r-- | public/js/components.js | 12 | ||||
-rw-r--r-- | public/js/domain.js | 95 | ||||
-rw-r--r-- | public/js/home.js | 91 | ||||
-rw-r--r-- | public/js/login.js | 44 | ||||
-rw-r--r-- | public/js/main.js | 136 | ||||
-rw-r--r-- | public/login.html | 21 | ||||
-rw-r--r-- | public/robots.txt | 9 |
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 Binary files differnew file mode 100644 index 0000000..332b66c --- /dev/null +++ b/public/fonts/helvetica-bold.ttf diff --git a/public/fonts/helvetica.ttf b/public/fonts/helvetica.ttf Binary files differnew file mode 100644 index 0000000..718f22d --- /dev/null +++ b/public/fonts/helvetica.ttf diff --git a/public/fonts/overpass-bold-italic.otf b/public/fonts/overpass-bold-italic.otf Binary files differnew file mode 100644 index 0000000..f4929ae --- /dev/null +++ b/public/fonts/overpass-bold-italic.otf diff --git a/public/fonts/overpass-bold.otf b/public/fonts/overpass-bold.otf Binary files differnew file mode 100644 index 0000000..962a5d7 --- /dev/null +++ b/public/fonts/overpass-bold.otf 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, '<').replace(/>/g, '>'); + 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 |