diff options
Diffstat (limited to 'public/js')
-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 |
6 files changed, 429 insertions, 0 deletions
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 |