initial upload

This commit is contained in:
Kai Waggeling 2025-05-17 16:20:29 +02:00
parent 987c99d00b
commit bb6c0147db
44 changed files with 1884 additions and 131 deletions

133
.gitignore vendored
View file

@ -1,132 +1,3 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/ node_modules/
jspm_packages/ logs/
package-lock.json
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

6
assets/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
assets/css/tabler-icons.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

65
assets/start.js Normal file
View file

@ -0,0 +1,65 @@
const { loadModule } = window['vue3-sfc-loader'];
const options = {
moduleCache: {
vue: Vue,
},
getFile(url) {
return fetch(url).then((resp) =>
resp.ok ? resp.text() : Promise.reject(resp)
);
},
addStyle(styleStr) {
const style = document.createElement('style');
style.textContent = styleStr;
const ref = document.head.getElementsByTagName('style')[0] || null;
document.head.insertBefore(style, ref);
},
};
var vueApp = Vue.createApp({
data() {
return {
modeEdit: false,
elements: [{
id: '234bi23b4ikj',
type: 'jumbotron',
title: 'my super awesome title',
text: 'there is nothing better on this planet'
},
{
id: '234bi23b4ikj',
type: 'grid',
title: 'Group 1',
columns: 2,
elements: [{
id: '234bi23b4ikj',
type: 'link',
link: 'https://google.com/',
title: 'Tisoware',
description: 'Elektronische Arbeitszeiterfassung'
},
{
id: '234bi23b4ikj',
type: 'link',
link: 'https://google.com/',
title: 'IT Helpdesk',
description: 'Helpdesk der operativen IT für Supportanfragen'
}]
}]
}
},
methods: {
toggleEditor()
{
this.modeEdit = !this.modeEdit;
}
},
components: {
DynamicRoot: Vue.defineAsyncComponent(() =>
loadModule('/vue/root.vue', options)
),
},
}).mount('#app');

33
assets/vue/grid.vue Normal file
View file

@ -0,0 +1,33 @@
<template>
<div :class="[
'grid',
'grid-cols-1',
'md:grid-cols-2',
'lg:grid-cols-' + columns,
'gap-6',
'relative'
]">
<div v-if="title" class="col-span-full">
<h5 class="font-medium text-gray-700 dark:text-gray-300 text-2xl">{{ title }}</h5>
</div>
<button v-if="this.$root.modeEdit" type="button" title="Edit Element"
class="text-white bg-blue-700 hover:bg-blue-800 outline-none rounded-lg text-center dark:bg-blue-600 dark:hover:bg-blue-700 absolute top-2 right-2">
<i class="ti ti-edit mx-3 my-2 block"></i>
</button>
<slot>
</div>
</template>
<script>
export default {
data() {
return {
};
},
props: {
columns: Number,
title: String
},
};
</script>

28
assets/vue/jumbotron.vue Normal file
View file

@ -0,0 +1,28 @@
<template>
<section class="w-full relative">
<button v-if="this.$root.modeEdit" type="button" title="Edit Element" class="text-white bg-blue-700 hover:bg-blue-800 outline-none rounded-lg text-center dark:bg-blue-600 dark:hover:bg-blue-700 absolute top-2 right-2">
<i class="ti ti-edit mx-3 my-2 block"></i>
</button>
<div class="py-4 px-4 mx-auto max-w-screen-xl text-center">
<h1 class="my-4 font-extrabold tracking-tight leading-none text-4xl md:text-5xl lg:text-6xl">
{{ title }}
</h1>
<p class="my-4 text-lg font-normal text-gray-500 lg:text-xl sm:px-16 lg:px-48">
<slot>
</p>
</div>
</section>
</template>
<script>
export default {
data() {
return {
};
},
props: {
title: String
},
};
</script>

25
assets/vue/link.vue Normal file
View file

@ -0,0 +1,25 @@
<template>
<a :href="link" target="_blank" class="p-8 border rounded-md bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-600 hover:border-blue-400 ">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-gray-100">
{{ title }}
</h5>
<p class="font-normal text-gray-500 dark:text-gray-400 text-md">
{{ description }}
</p>
</a>
</template>
<script>
export default {
data() {
return {
};
},
props: {
link: String,
title: String,
description: String
},
};
</script>

45
assets/vue/root.vue Normal file
View file

@ -0,0 +1,45 @@
<template>
<template v-for="(element, elementIndex) in elements">
<div class="container flex justify-end">
<button v-if="this.$root.modeEdit" type="button" class="text-white bg-blue-700 hover:bg-blue-800 outline-none rounded-lg text-center dark:bg-blue-600 dark:hover:bg-blue-700 absolute top-2 right-2" title="Edit Element">
<i class="ti ti-edit mx-3 my-2 block"></i>
</button>
</div>
<tailwind-jumbotron v-if="element.type == 'jumbotron'" :title="element.title">
{{ element.text }}
</tailwind-jumbotron>
<tailwind-grid v-if="element.type == 'grid'" :columns="element.columns" :title="element.title">
<dynamic-root :elements="element.elements"></dynamic-root>
</tailwind-grid>
<tailwind-link v-if="element.type == 'link'" :link="element.link" :title="element.title" :description="element.description">
</tailwind-link>
</template>
</template>
<script>
export default {
data() {
return {
};
},
props: {
elements: Array
},
components: {
TailwindJumbotron: Vue.defineAsyncComponent(() =>
loadModule('/vue/jumbotron.vue', options)
),
TailwindGrid: Vue.defineAsyncComponent(() =>
loadModule('/vue/grid.vue', options)
),
TailwindLink: Vue.defineAsyncComponent(() =>
loadModule('/vue/link.vue', options)
),
DynamicRoot: Vue.defineAsyncComponent(() =>
loadModule('/vue/root.vue', options)
),
},
};
</script>

View file

@ -0,0 +1,11 @@
let jumbotron = Vue.defineComponent('tailwind-jumbotron',
{
props: {
title: String,
text: String
},
template: '#template-jumbotron'
});
// customElements.define('tailwind-jumbotron', jumbotron)

4
config/settings.yaml Normal file
View file

@ -0,0 +1,4 @@
serverPort: 3000
serverAddr: 0.0.0.0
trustProxy:
- loopback

View file

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFlzCCA3+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBwMR0wGwYDVQQDExR3YWdn
ZWxpbmcubmV0IHJvb3RDQTELMAkGA1UEBhMCREUxEDAOBgNVBAgTB1NhY2hzZW4x
EDAOBgNVBAcTB0xlaXB6aWcxDzANBgNVBAoTBldDbG91ZDENMAsGA1UECxMEVEVz
dDAeFw0yNDA3MjEwODA5MjFaFw0zNDA3MjEyMDA5MjFaMHAxHTAbBgNVBAMTFHdh
Z2dlbGluZy5uZXQgcm9vdENBMQswCQYDVQQGEwJERTEQMA4GA1UECBMHU2FjaHNl
bjEQMA4GA1UEBxMHTGVpcHppZzEPMA0GA1UEChMGV0Nsb3VkMQ0wCwYDVQQLEwRU
RXN0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3QBOFwZr5Rb728Va
IksHQ45lebxKbt5JkdAXT90E+Bk9/DWE0q6LLf4moWmK7oJ0iULZKufisAnrvrif
VsrGBpmgUS7neN9xUT8fysGiLAETfcApqk9l0StO0RDAz3fyKTTnPpffUo927uHy
aulkUNUSFJ1oFvz4gFPt35YreROmZlRHVtZcz8IEwzERs+ocv1cEssVB7lH8e5OD
ATtmyN0+92JnU7JYswM9GCbK4G6YAMNNvMuEPx9kH1MFVMrXvja59ebKRb/lrYFP
43xdu0mQzT3S+l5WYJjdx9+ZSp8nnjMs/YgpSNqkOubVdmV1gTikj64R7OLPqIYB
HP4c2pokxLNfs2j2APrEG5pPA7KhrFJOnx/IxfP7I5DdXA1ycr8c8r/OZ+eJnVL6
jcQp4B4MU+Jhs3ruBX6mVJNYQz+HULUYVjFEXNlbXhbTLovKScxtE8P4Wof7pIgA
m2UpXJnroWmf79JCckuuHnzGGFEGtud3JGDxRy5Z8g6nutcKM1nydetDs9IKRHwY
BS+Rx/9aGWQ9cfN6/R+gKsxcuhU3zhDL+r2Z/aBb9NNNEEYaLJGbJJQglkVszX2u
8hCxp5/ISyI+mG5R4eRQ4zx6ME8Hw++vdR6LcKfVtnIiIRtBf06PtWF78QmD+y4w
zRM2JSzcGyHQ1+Jl8Bqak3z1bSUCAwEAAaM8MDowDAYDVR0TBAUwAwEB/zALBgNV
HQ8EBAMCAQYwHQYDVR0OBBYEFOQvL+Ppyd26+3YlnxS5tfVAaAxjMA0GCSqGSIb3
DQEBCwUAA4ICAQByUM6GkY3v7tPGMMNmRIjyvhhm2Tl68aPL21ctfkATetMxsaGj
LtDqkiCi+Vux68f0H1IQ8zI835b0Qr++8oi5qRldVjF7ceBoqnIT3ofxik2qT+HB
fG0/BisKG6T+xA9TaJ0/swwBtk5FNIBq1J/5LlUPoMb0M8gM3yr6B5xRr2YJiu4b
4fghYeKjM6I+z/tFNFKeY18W3yzn1ViuKuc+x6qtuTWsoCwy20S9TrtgIwa8uUha
SVctN57ETB3RGHbsnGcUUL69XdKMuhMDZ89SJAHluQL2cep/ee/v8A6HWcgXYt7q
wPmFrHVr/gycjMk4B4+XVamwuJkY2gtPVVEv3wfbp8FQm6bgS2LBrw3+ERYXynW0
3UteQE82vP6Yi2hPZQL4KdCO41P7jaiANXCjQtNFcJQsMUntUdGnG7rcAIeVQ4QZ
fH64bgqpR4UEZK1KYJpvHLZRVsXrrDkjoUpCqhqz4Y0aQnHnL8NGYfgT/HAugbJW
MhaKh81QfkJ3ff4AY30ubFyYuTr9tJst12vPZGM3tfnvvgmO49dR1Zdm5tLqcNe2
lEC3qH1rTSz+W7QsA0CKhgp823hWOEvt7E/sypeupdIXT2+aMGdN6ougGTZYSDTY
9HfTYTTeQdKxSwB3UbRDcMJgAJPsgf65rzbbri8JW2hKcfghiV39aDM8FA==
-----END CERTIFICATE-----

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,6 @@
{
"1a278ace": {
"displayName": "waggeling.net rootCA",
"certificateType": "rootCA"
}
}

View file

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,58 @@
import {
JsonDB,
Config
} from 'node-json-db';
import {
default as ValidateSchema
} from 'validate'
import {
randomBytes
} from "crypto";
var authorityDB = new JsonDB(new Config("datastore/database/authorityDB.json", true, true, '/'));
const authoritySchema = new ValidateSchema({
name: {
type: String,
required: true,
match: /^[a-zA-Z0-9\ \-\_]+$/,
length: { min: 3, max: 32 },
message: 'invalid name'
}
})
export async function saveAuthority(authorityID, authorityConfig) {
let configErrorList = authoritySchema.validate(authorityConfig);
configErrorList = configErrorList.map((configError) => {
return configError.message;
})
if (configErrorList.length > 0) {
return {
status: "error",
errors: configErrorList
}
}
await authorityDB.push(`/${authorityID}`, authorityConfig);
return {
status: "ok"
}
}
export async function getAuthority(authorityID) {
let AuthorityData = await authorityDB.getData(`/${authorityID}`);
return AuthorityData;
}
export async function deleteAuthority(authorityID) {
await authorityDB.delete(`/${authorityID}`);
}
saveAuthority(randomBytes(4).toString("hex"), {names: "{test}"})

View file

@ -0,0 +1,91 @@
import {
JsonDB,
Config
} from 'node-json-db';
import {
default as ValidateSchema
} from 'validate'
var certificateDB = new JsonDB(new Config("datastore/database/certificates.json", true, true, '/'));
const certificateSchema = new ValidateSchema({
displayName: {
type: String,
required: true,
match: /^[a-zA-Z0-9\ \-\_\.]+$/,
message: {
type: '[displayName] must be a string.',
required: '[displayName] is required.',
match: '[displayName] is invalid.'
}
},
certificateType: {
type: String,
required: true,
enum: [
'rootCA',
'intermediateCA',
'identity'
],
message: {
type: '[certificateType] must be a string.',
required: '[certificateType] is required.',
enum: '[certificateType] must be one of [rootCA|intermediateCA|identity]'
}
}
})
export async function saveCertificate(sCertificateID, oCertificateData) {
console.log("-----------------------------------------");
let configErrorList = certificateSchema.validate(oCertificateData);
configErrorList = configErrorList.map((configError) => {
return configError.message;
})
console.log(configErrorList);
if (configErrorList.length > 0) {
return {
status: "error",
errors: configErrorList
}
}
await certificateDB.push(`/${sCertificateID}`, oCertificateData);
console.log(oCertificateData);
setTimeout(() => {
}, 5000);
return {
status: "ok"
}
}
export async function getCertificateByID(sCertificateID) {
let oCertificateData = await certificateDB.getData(`/${sCertificateID}`);
return oCertificateData;
}
export async function getAllCertificates() {
let aCertificateList = await certificateDB.getData(`/`);
return aCertificateList;
}
export async function getCertificatesOfType(sCertificateType) {
let aCertificateList = await certificateDB.getData(`/`);
aCertificateList = aCertificateList.filter((oCertificateData) => {
return oCertificateData.certificateType == sCertificateType;
})
return aCertificateList;
}
export async function deleteCertificate(sCertificateID) {
await certificateDB.delete(`/${sCertificateID}`);
}

134
functions/db.users.mjs Normal file
View file

@ -0,0 +1,134 @@
import {
JsonDB,
Config
} from 'node-json-db';
import {
default as ValidateSchema
} from 'validate'
var userDB = new JsonDB(new Config("datastore/database/userDB.json", true, true, '/'));
const userSchema = new ValidateSchema({
username: {
type: String,
required: true,
match: /^[a-zA-Z0-9\ \-\_]+$/,
length: { min: 3, max: 32 },
message: {
type: '[username] must be of type String.',
required: '[username] is required.',
match: '[username] is invalid. Allowed characters: (a-z|A-Z|0-9| |-|_)',
length: '[username] must consist of 3 to 32 characters.'
}
},
firstName: {
type: String,
required: true,
match: /^[a-zA-Z0-9\ \-\_]+$/,
length: { min: 1, max: 32 },
message: {
type: '[firstName] must be of type String.',
required: '[firstName] is required.',
match: '[firstName] is invalid. Allowed characters: (a-z|A-Z|0-9| |-|_)',
length: '[firstName] must consist of 1 to 32 characters.'
}
},
lastName: {
type: String,
required: true,
match: /^[a-zA-Z0-9\ \-\_]+$/,
length: { min: 1, max: 32 },
message: {
type: '[lastName] must be of type String.',
required: '[lastName] is required.',
match: '[lastName] is invalid. Allowed characters: (a-z|A-Z|0-9| |-|_)',
length: '[lastName] must consist of 1 to 32 characters.'
}
},
emailAddress: {
type: String,
required: true,
match: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/,
message: {
type: '[emailAddress] must be of type String.',
required: '[emailAddress] is required.',
match: '[emailAddress] is invalid (e.g. admin@example.com).'
}
},
enabled: {
type: String,
required: true,
enum: [
'true',
'false'
],
message: {
type: '[enabled] must be of type String.',
required: '[enabled] is required.',
enum: '[enabled] must be one of [true|false].'
}
},
permissions: {
type: Array,
required: true,
elements: [{
type: String
}],
message: {
type: '[permissions] must be of type Array.',
required: '[permissions] is required.',
elements: '[permissions] elements must be of type String.'
}
}
})
export async function saveUser(sUserID, oUserData) {
let dataErrorList = userSchema.validate(oUserData);
dataErrorList = dataErrorList.map((configError) => {
return configError.message;
})
if (dataErrorList.length > 0) {
return {
status: "error",
errors: dataErrorList
}
}
await userDB.push(`/${sUserID}`, oUserData);
return {
status: "ok"
}
}
export async function getUser(userID) {
let requestData = await userDB.getData(`/${userID}`);
return {
username: requestData.username,
firstName: requestData.firstName,
lastName: requestData.lastName,
emailAddress: requestData.emailAddress
};
}
export async function getAllUsers() {
let userList = await userDB.getData(`/`);
return userList.map((userData) => {
return {
username: userData.username,
firstName: userData.firstName,
lastName: userData.lastName,
emailAddress: userData.emailAddress
}
})
}
export async function deleteUser(userID) {
await userDB.delete(`/${userID}`);
}

View file

@ -0,0 +1,84 @@
import {
default as forge
} from "node-forge";
import {
writeFileSync,
mkdirSync
} from "fs";
import {
randomBytes
} from "crypto";
// Funktion zur Generierung und Speicherung eines Root-CA-Zertifikats
export function generateRootCA(Params) {
const uid = randomBytes(4).toString("hex");
// Generiere ein neues Schlüsselpaar
const keys = forge.pki.rsa.generateKeyPair(4096);
// Erstelle ein neues Zertifikat
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
// cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
const attrs = [{
name: 'commonName',
value: 'My Root CA'
}, {
name: 'countryName',
value: 'US'
}, {
shortName: 'ST',
value: 'California'
}, {
name: 'localityName',
value: 'San Francisco'
}, {
name: 'organizationName',
value: 'My Organization'
}, {
shortName: 'OU',
value: 'My Organizational Unit'
}];
cert.setSubject(attrs);
cert.setIssuer(attrs);
// Erweiterungen hinzufügen
cert.setExtensions([{
name: 'basicConstraints',
cA: true
}, {
name: 'keyUsage',
keyCertSign: true,
cRLSign: true
}, {
name: 'subjectKeyIdentifier'
}]);
// Zertifikat mit dem privaten Schlüssel signieren
cert.sign(keys.privateKey, forge.md.sha256.create());
// Zertifikat und Schlüssel als PEM kodieren
const pemCert = forge.pki.certificateToPem(cert);
const pemPrivateKey = forge.pki.privateKeyToPem(keys.privateKey);
const pemPublicKey = forge.pki.publicKeyToPem(keys.publicKey);
// Erstelle das RootCA Verzeichnis
mkdirSync(`datastore/certificates/${uid}`)
// Zertifikat und Schlüssel in Dateien speichern
writeFileSync(`datastore/certificates/${uid}/rootCA.crt`, pemCert);
writeFileSync(`datastore/certificates/${uid}/rootCA.key`, pemPrivateKey);
writeFileSync(`datastore/certificates/${uid}/rootCA.pub`, pemPublicKey);
console.log('Root CA-Zertifikat und Schlüssel wurden generiert und gespeichert.');
}
// Funktion aufrufen, um das Root-CA-Zertifikat zu generieren
// generateRootCA();

View file

@ -0,0 +1,77 @@
import {
default as forge
} from "node-forge";
import {
writeFileSync,
mkdirSync
} from "fs";
import {
randomBytes
} from "crypto";
// Funktion zur Generierung und Speicherung eines Root-CA-Zertifikats
export function generateRootCA(Params) {
const uid = randomBytes(4).toString("hex");
// Generiere ein neues Schlüsselpaar
const keys = forge.pki.rsa.generateKeyPair(4096);
// Erstelle einen neuen CSR
const csr = forge.pki.createCertificationRequest();
// Setze den öffentlichen Schlüssel
csr.publicKey = keys.publicKey;
// Setze die CSR Attribute
csr.setSubject([{
name: 'commonName',
value: 'example.com'
}, {
name: 'countryName',
value: 'US'
}, {
shortName: 'ST',
value: 'California'
}, {
name: 'localityName',
value: 'San Francisco'
}, {
name: 'organizationName',
value: 'Example, Inc.'
}, {
shortName: 'OU',
value: 'IT'
}]);
// Signiere die CSR mit dem privaten Schlüssel
csr.sign(keys.privateKey, forge.md.sha256.create());
// Überprüfe die CSR
const verified = csr.verify();
if (verified) {
console.log('CSR verification successful');
} else {
console.error('CSR verification failed');
return;
}
// CSR und Schlüssel als PEM kodieren
const pemCsr = forge.pki.certificationRequestToPem(csr);
const pemPrivateKey = forge.pki.privateKeyToPem(keys.privateKey);
// Erstelle das RootCA Verzeichnis
mkdirSync(`datastore/certificates/${uid}`)
// Zertifikat und Schlüssel in Dateien speichern
writeFileSync(`datastore/certificates/${uid}/rootCA.crt`, pemCert);
writeFileSync(`datastore/certificates/${uid}/rootCA.key`, pemPrivateKey);
writeFileSync(`datastore/certificates/${uid}/rootCA.pub`, pemPublicKey);
console.log('Root CA-Zertifikat und Schlüssel wurden generiert und gespeichert.');
}
// Funktion aufrufen, um das Root-CA-Zertifikat zu generieren
// generateRootCA();

View file

@ -0,0 +1,298 @@
import {
default as forge
} from "node-forge";
import {
loadCertificate,
loadPrivateKey,
saveCertificate,
savePrivateKey
} from "./pki.utils.mjs";
import {
saveCertificate as saveCertificateToDatabase
} from "./db.certificates.mjs";
import {
default as ValidateSchema
} from 'validate'
import {
default as parseDuration
} from "parse-duration";
import {
randomBytes
} from "crypto";
import { error } from "console";
const CertificateSchema = new ValidateSchema({
certType: {
type: String,
required: true,
enum: [
'rootCA',
'intermediateCA',
'identity'
],
message: {
type: '[certType] must be a string.',
required: '[certType] is required.',
enum: '[certType] must be one of [authority, identity]'
}
},
parent: {
type: String,
required: false,
message: {
type: '[parent] must be a string.'
}
},
duration: {
type: String,
required: true,
message: {
type: '[duration] must be a string.',
required: '[duration] is required.'
}
},
commonName: {
type: String,
required: true,
match: /^[a-zA-Z0-9\ \-\_\.]+$/,
length: { min: 3, max: 32 },
message: {
type: '[commonName] must be a string.',
required: '[commonName] is required.',
match: '[commonName] is invalid.',
length: '[commonName] is invalid.'
}
},
emailAddress: {
type: String,
required: true,
match: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/,
message: {
type: '[emailAddress] must be a string.',
required: '[emailAddress] is required.',
match: '[emailAddress] is invalid (e.g. admin@example.com).'
}
},
countryName: {
type: String,
required: true,
match: /^[A-Z]{2}$/,
message: {
type: '[countryName] must be a string.',
required: '[countryName] is required.',
match: '[countryName] needs to be 2 letter code (e.g. US).'
}
},
stateOrProvinceName: {
type: String,
required: true,
match: /^[A-Za-z ]{3,64}$/,
message: {
type: '[stateOrProvinceName] must be a string.',
required: '[stateOrProvinceName] is required.',
match: '[stateOrProvinceName] is invalid: (A-Z|a-z| ), 3-64 characters'
}
},
localityName: {
type: String,
required: true,
match: /^[A-Za-z ]{3,64}$/,
message: {
type: '[localityName] must be a string.',
required: '[localityName] is required.',
match: '[localityName] is invalid: (A-Z,a-z, ), 3-64 characters'
}
},
organizationName: {
type: String,
required: true,
match: /^[A-Za-z\ \-\_\.]{3,64}$/,
message: {
type: '[organizationName] must be a string.',
required: '[organizationName] is required.',
match: '[organizationName] is invalid: (A-Z,a-z, ), 3-64 characters'
}
},
organizationalUnitName: {
type: String,
required: true,
match: /^[A-Za-z\ \-\_\.]{3,64}$/,
message: {
type: '[organizationalUnitName] must be a string.',
required: '[organizationalUnitName] is required.',
match: '[organizationalUnitName] is invalid: (A-Z,a-z, ,.,-,_), 3-64 characters'
}
}
})
function generateCertificate(Params) {
let configErrorList = CertificateSchema.validate(Params);
configErrorList = configErrorList.map((configError) => {
return configError.message;
})
if (configErrorList.length > 0) {
return {
status: "error",
errors: configErrorList
}
}
// generate certificate id
const certificateId = randomBytes(4).toString("hex");
// generate new keypair
const {
privateKey,
publicKey
} = forge.pki.rsa.generateKeyPair(4096);
// initialize new certificate
const certificate = forge.pki.createCertificate();
certificate.publicKey = publicKey;
// cert.serialNumber = '01';
certificate.validity.notBefore = new Date(Date.now());
certificate.validity.notAfter = new Date(Date.now() + parseDuration(Params.duration));
const attrs = [{
name: 'commonName',
value: Params.commonName
}, {
name: 'countryName',
value: Params.countryName
}, {
name: 'stateOrProvinceName',
value: Params.stateOrProvinceName
}, {
name: 'localityName',
value: Params.localityName
}, {
name: 'organizationName',
value: Params.organizationName
}, {
name: 'organizationalUnitName',
value: Params.organizationalUnitName
}];
certificate.setSubject(attrs);
certificate.setIssuer(attrs);
// Erweiterungen hinzufügen
certificate.setExtensions([{
name: 'basicConstraints',
cA: true
}, {
name: 'keyUsage',
keyCertSign: true,
cRLSign: true
}, {
name: 'subjectKeyIdentifier'
}]);
console.log(`new certificate with uid ${certificateId} generated`);
return {
status: "ok",
certificateId,
certificate,
privateKey,
publicKey
}
}
export async function createRootCertificate(certificateParameters) {
let oGeneratedData = generateCertificate({
certType: 'rootCA',
parent: null,
duration: certificateParameters.duration,
emailAddress: certificateParameters.emailAddress,
commonName: certificateParameters.commonName,
countryName: certificateParameters.countryName,
stateOrProvinceName: certificateParameters.stateOrProvinceName,
localityName: certificateParameters.localityName,
organizationName: certificateParameters.organizationName,
organizationalUnitName: certificateParameters.organizationalUnitName
});
if (oGeneratedData.status != "ok") {
return {
status: "error",
errors: oGeneratedData.errors
}
}
// sign the new certificate
oGeneratedData.certificate.sign(oGeneratedData.privateKey, forge.md.sha256.create());
// save certificate and keys
saveCertificate(oGeneratedData.certificate, oGeneratedData.certificateId, 'rootCA');
savePrivateKey(oGeneratedData.privateKey, oGeneratedData.certificateId, 'rootCA');
saveCertificateToDatabase(oGeneratedData.certificateId, {
displayName: certificateParameters.commonName,
certificateType: 'rootCA'
})
console.log(`new rootCA certificate with uid ${oGeneratedData.certificateId} saved`);
return {
status: "ok",
certificateId: oGeneratedData.certificateId
}
}
export async function createIntermediateCertificate(oCertificateParameters, sRootCertificateId) {
let {
certificateId: newCertificateId,
certificate: newCertificate,
privateKey: newPrivateKey
} = generateCertificate(oCertificateParameters);
// load certificate authority certificate and privateKey
const rootCACertificate = loadCertificate(sRootCertificateId);
const rootCAPrivateKey = loadPrivateKey(sRootCertificateId);
// set rootCA as issuer
newCertificate.setIssuer(rootCACertificate.subject.attributes);
// sign certificate with root certificate
newCertificate.sign(rootCAPrivateKey, forge.md.sha256.create());
// save certificate and keys
saveCertificate(newCertificate, newCertificateId, 'intermediateCA');
savePrivateKey(newPrivateKey, newCertificateId, 'intermediateCA');
saveCertificateToDatabase(newCertificateId, {
displayName: Params.commonName,
certificateType: 'intermediateCA'
})
console.log(`new intermediateCA certificate with uid ${newCertificateId} saved`);
}
export async function createIdentityCertificate(Params) {
let {
certificateId,
certificate,
privateKey,
publicKey
} = generateCertificate(Params)
}
// Funktion aufrufen, um das Root-CA-Zertifikat zu generieren
console.log((await createRootCertificate({
duration: '10y',
emailAddress: 'kai@waggeling.net',
commonName: 'waggeling.net rootCA',
countryName: 'DE',
stateOrProvinceName: 'Sachsen',
localityName: 'Leipzig',
organizationName: 'WCloud',
organizationalUnitName: 'TEst'
})));

51
functions/pki.utils.mjs Normal file
View file

@ -0,0 +1,51 @@
import {
default as forge
} from "node-forge";
import {
readFileSync,
writeFileSync,
mkdirSync
} from "fs";
export function loadCertificate(sCertID) {
return forge.pki.certificateFromPem(
readFileSync(`datastore/certificates/${sCertID}/certificate.pem`, 'utf8')
);
}
export function loadPrivateKey(sCertID) {
return forge.pki.privateKeyFromPem(
readFileSync(`datastore/certificates/${sCertID}/privateKey.pem`, 'utf8')
);
}
export function saveCertificate(iCertificate, sCertID, sCertType) {
// encode certificates as PEM
const certificate = forge.pki.certificateToPem(iCertificate);
// create certificate directory
mkdirSync(`datastore/certificates/`, { recursive: true });
// write certificate to file
writeFileSync(`datastore/certificates/${sCertID}.crt`, certificate);
}
export function savePrivateKey(iPrivateKey, sCertID) {
// encode keys as PEM
const privateKey = forge.pki.privateKeyToPem(iPrivateKey);
// create key directory
mkdirSync(`datastore/keys/`, { recursive: true });
// write keys to file
writeFileSync(`datastore/keys/${sCertID}.key`, privateKey);
}
export function generatePublicKeyFromPrivateKey(iPrivateKey) {
return forge.pki.rsa.setPublicKey(iPrivateKey.n, iPrivateKey.e);
}

92
functions/structure.mjs Normal file
View file

@ -0,0 +1,92 @@
import {
Op
} from "sequelize";
import {
ElementTable,
AttributeTable
} from "../database/models/structure.mjs";
export async function CreateDirectory(parentId, name)
{
let ParentDirectory = await ElementTable.findByPk(parentId);
if (ParentDirectory == null) {
var NewDirectory = await ElementTable.create({
type: 'directory'
});
} else {
var NewDirectory = await ParentDirectory.createChild({
type: 'directory'
});
}
await NewDirectory.createAttribute({
version: NewDirectory.latestVersion,
contentType: 'name',
contentValue: name
})
return NewDirectory;
}
export async function GetDirectory(directoryId, version = null)
{
let Directory = await ElementTable.findByPk(directoryId);
if (Directory == null) {
return null;
}
if (version == null) {
version = Directory.latestVersion
}
let Attributes = await Directory.getAttribute()
var Result = {
id: Directory.id,
version
}
Attributes.forEach((Attribute) => {
Result[Attribute.contentType] = Attribute.contentValue
});
return Result;
}
export async function GetDirectories(parentId = null)
{
let Directories = await ElementTable.findAll({
where: {
[Op.and]: {
type: 'directory',
parentId: parentId
}
}
});
Directories = Directories.map(async (Directory) => {
let Attributes = await Directory.getAttribute()
var Result = {
id: Directory.id,
version: Directory.latestVersion
}
console.log(Attributes);
Attributes.forEach((Attribute) => {
Result[Attribute.contentType] = Attribute.contentValue
});
return Result;
})
await Promise.all(Directories);
return Directories;
}

43
master.mjs Normal file
View file

@ -0,0 +1,43 @@
// Load WebServer
import {
default as express
} from "express";
import {
router
} from "express-file-routing"
// Load Templating Engine
import nunjucks from "nunjucks";
// Initialize WebServer
const expressApp = express();
const expressPort = 3000;
// Middleware, um CORS zu aktivieren
expressApp.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
next();
});
// Configure Templating Engine
nunjucks.configure('./', {
autoescape: false,
express: expressApp,
noCache: true
});
// Mount Middlewares to WebServer
expressApp.use(express.json());
expressApp.use(express.urlencoded());
expressApp.use(express.static('./assets/'))
// Mount Routes to WebServer
expressApp.use("/", await router())
// Server starten
expressApp.listen(expressPort, () => {
console.log(`Server is listening on port ${expressPort}`);
});

20
package.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "script-server",
"version": "1.0.0",
"description": "",
"main": "master.mjs",
"scripts": {
"start": "npm i && node ./master.mjs"
},
"keywords": [],
"author": "Kai Uwe Waggeling",
"license": "ISC",
"dependencies": {
"express": "^5.0.1",
"express-file-routing": "^3.0.3",
"node-json-db": "^2.3.0",
"nunjucks": "^3.2.4",
"validate": "^5.2.0",
"yaml": "^2.7.0"
}
}

84
routes/api.queue.mjs Normal file
View file

@ -0,0 +1,84 @@
import {
Router
} from "express";
export var Routes = Router();
// Routes.get('/queues', async function (Request, Response)
// {
// let QueueList = await QueueTable.findAll();
// QueueList.sort(function (a, b) {
// if (a.name < b.name) return -1;
// if (a.name > b.name) return 1;
// return 0;
// });
// Response.status(200);
// Response.send(QueueList);
// })
// Routes.get('/queue/:QueueID', async function (Request, Response)
// {
// let Queue = await QueueTable.findByPk(Request.params.QueueID);
// Response.status(200);
// Response.send(Queue);
// })
// Routes.post('/queue', async function (Request, Response)
// {
// let Queue = await QueueTable.create({
// name: Request.body.name
// });
// await Queue.setPrinter(Request.body.printerId)
// Response.status(200);
// Response.send(Queue);
// })
// Routes.put('/queue', async function (Request, Response)
// {
// let Queue = await QueueTable.findByPk(Request.body.id);
// await Queue.update({
// name: Request.body.name
// });
// if (Request.body.printerId == "null") {
// await Queue.setPrinter(null);
// } else {
// await Queue.setPrinter(Request.body.printerId)
// }
// Response.status(200);
// Response.send(Queue);
// })
// Routes.delete('/queue/:QueueID', async function (Request, Response)
// {
// await (await QueueTable.findByPk(Request.params.QueueID)).destroy();
// Response.status(200);
// Response.end();
// })
// Routes.put('/queue/printer', async function (Request, Response)
// {
// let Queue = await QueueTable.findByPk(Request.params.queueId);
// let Printer = await PrinterTable.findByPk(Request.params.printerId);
// await Queue.setPrinter(Printer);
// Response.status(200);
// Response.end();
// })

6
routes/master.mjs Normal file
View file

@ -0,0 +1,6 @@
export const get = async (request, response) => {
if (request.method !== "GET") return response.status(405)
response.render(`ui/master.njk`);
}

153
ui/components/editor.njk Normal file
View file

@ -0,0 +1,153 @@
<!-- drawer component -->
<div id="drawer-form"
class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white w-80 dark:bg-gray-800"
tabindex="-1" aria-labelledby="drawer-form-label">
<h5 id="drawer-label"
class="inline-flex items-center mb-6 text-base font-semibold text-gray-500 uppercase dark:text-gray-400"><svg
class="w-3.5 h-3.5 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 20 20">
<path
d="M0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm14-7.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm0 4a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm-5-4a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm0 4a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm-5-4a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm0 4a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4Z" />
</svg>New event</h5>
<button type="button" data-drawer-hide="drawer-form" aria-controls="drawer-form"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 inline-flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close menu</span>
</button>
<form class="mb-6">
<div class="mb-6">
<label for="title" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Title</label>
<input type="text" id="title"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Apple Keynote" required />
</div>
<div class="mb-6">
<label for="description"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
<textarea id="description" rows="4"
class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Write event description..."></textarea>
</div>
<div class="relative mb-6">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path
d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z" />
</svg>
</div>
<input datepicker="" datepicker-autohide datepicker-buttons="" type="text"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 datepicker-input"
placeholder="Select date">
</div>
<div class="mb-4">
<label for="guests" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Invite
guests</label>
<div class="relative">
<input type="search" id="guests"
class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Add guest email" required />
<button type="button"
class="absolute inline-flex items-center px-3 py-1 text-sm font-medium text-white bg-blue-700 rounded-lg end-2 bottom-2 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"><svg
class="w-3 h-3 me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 20 18">
<path
d="M6.5 9a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9ZM8 10H5a5.006 5.006 0 0 0-5 5v2a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-2a5.006 5.006 0 0 0-5-5Zm11-3h-2V5a1 1 0 0 0-2 0v2h-2a1 1 0 1 0 0 2h2v2a1 1 0 0 0 2 0V9h2a1 1 0 1 0 0-2Z" />
</svg>Add</button>
</div>
</div>
<div class="flex mb-4 -space-x-4 rtl:space-x-reverse">
<img class="w-8 h-8 border-2 border-white rounded-full dark:border-gray-800"
src="/docs/images/people/profile-picture-5.jpg" alt="">
<img class="w-8 h-8 border-2 border-white rounded-full dark:border-gray-800"
src="/docs/images/people/profile-picture-2.jpg" alt="">
<img class="w-8 h-8 border-2 border-white rounded-full dark:border-gray-800"
src="/docs/images/people/profile-picture-3.jpg" alt="">
<img class="w-8 h-8 border-2 border-white rounded-full dark:border-gray-800"
src="/docs/images/people/profile-picture-4.jpg" alt="">
</div>
<button type="submit"
class="text-white justify-center flex items-center bg-blue-700 hover:bg-blue-800 w-full focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"><svg
class="w-3.5 h-3.5 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 20 20">
<path
d="M18 2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2ZM2 18V7h6.7l.4-.409A4.309 4.309 0 0 1 15.753 7H18v11H2Z" />
<path
d="M8.139 10.411 5.289 13.3A1 1 0 0 0 5 14v2a1 1 0 0 0 1 1h2a1 1 0 0 0 .7-.288l2.886-2.851-3.447-3.45ZM14 8a2.463 2.463 0 0 0-3.484 0l-.971.983 3.468 3.468.987-.971A2.463 2.463 0 0 0 14 8Z" />
</svg> Create event</button>
</form>
</div>
<!-- speed dial -->
<div data-dial-init class="fixed end-6 bottom-6 group">
<div id="editor-actions" class="flex flex-col items-center hidden mb-4 space-y-2">
<button type="button" data-tooltip-target="tooltip-share" data-tooltip-placement="left"
class="flex justify-center items-center w-[52px] h-[52px] text-gray-500 hover:text-gray-900 bg-white rounded-lg border border-gray-200 dark:border-gray-600 shadow-sm dark:hover:text-white dark:text-gray-400 hover:bg-gray-50 dark:bg-gray-700 dark:hover:bg-gray-600 focus:ring-4 focus:ring-gray-300 focus:outline-none dark:focus:ring-gray-400">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 18 18">
<path
d="M14.419 10.581a3.564 3.564 0 0 0-2.574 1.1l-4.756-2.49a3.54 3.54 0 0 0 .072-.71 3.55 3.55 0 0 0-.043-.428L11.67 6.1a3.56 3.56 0 1 0-.831-2.265c.006.143.02.286.043.428L6.33 6.218a3.573 3.573 0 1 0-.175 4.743l4.756 2.491a3.58 3.58 0 1 0 3.508-2.871Z" />
</svg>
<span class="sr-only">Share</span>
</button>
<div id="tooltip-share" role="tooltip"
class="absolute z-10 invisible inline-block w-auto px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
Share
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<button type="button" data-tooltip-target="tooltip-print" data-tooltip-placement="left"
class="flex justify-center items-center w-[52px] h-[52px] text-gray-500 hover:text-gray-900 bg-white rounded-lg border border-gray-200 dark:border-gray-600 shadow-sm dark:hover:text-white dark:text-gray-400 hover:bg-gray-50 dark:bg-gray-700 dark:hover:bg-gray-600 focus:ring-4 focus:ring-gray-300 focus:outline-none dark:focus:ring-gray-400">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 20 20">
<path d="M5 20h10a1 1 0 0 0 1-1v-5H4v5a1 1 0 0 0 1 1Z" />
<path
d="M18 7H2a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2v-3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2Zm-1-2V2a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3h14Z" />
</svg>
<span class="sr-only">Print</span>
</button>
<div id="tooltip-print" role="tooltip"
class="absolute z-10 invisible inline-block w-auto px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
Print
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<button type="button" data-tooltip-target="tooltip-download" data-tooltip-placement="left"
class="flex justify-center items-center w-[52px] h-[52px] text-gray-500 hover:text-gray-900 bg-white rounded-lg border border-gray-200 dark:border-gray-600 shadow-sm dark:hover:text-white dark:text-gray-400 hover:bg-gray-50 dark:bg-gray-700 dark:hover:bg-gray-600 focus:ring-4 focus:ring-gray-300 focus:outline-none dark:focus:ring-gray-400">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 20 20">
<path
d="M14.707 7.793a1 1 0 0 0-1.414 0L11 10.086V1.5a1 1 0 0 0-2 0v8.586L6.707 7.793a1 1 0 1 0-1.414 1.414l4 4a1 1 0 0 0 1.416 0l4-4a1 1 0 0 0-.002-1.414Z" />
<path
d="M18 12h-2.55l-2.975 2.975a3.5 3.5 0 0 1-4.95 0L4.55 12H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2Zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z" />
</svg>
<span class="sr-only">Download</span>
</button>
<div id="tooltip-download" role="tooltip"
class="absolute z-10 invisible inline-block w-auto px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
Download
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<button type="button" data-tooltip-target="tooltip-copy" data-tooltip-placement="left"
class="flex justify-center items-center w-[52px] h-[52px] text-gray-500 hover:text-gray-900 bg-white rounded-lg border border-gray-200 dark:border-gray-600 dark:hover:text-white shadow-sm dark:text-gray-400 hover:bg-gray-50 dark:bg-gray-700 dark:hover:bg-gray-600 focus:ring-4 focus:ring-gray-300 focus:outline-none dark:focus:ring-gray-400">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 18 20">
<path
d="M5 9V4.13a2.96 2.96 0 0 0-1.293.749L.879 7.707A2.96 2.96 0 0 0 .13 9H5Zm11.066-9H9.829a2.98 2.98 0 0 0-2.122.879L7 1.584A.987.987 0 0 0 6.766 2h4.3A3.972 3.972 0 0 1 15 6v10h1.066A1.97 1.97 0 0 0 18 14V2a1.97 1.97 0 0 0-1.934-2Z" />
<path
d="M11.066 4H7v5a2 2 0 0 1-2 2H0v7a1.969 1.969 0 0 0 1.933 2h9.133A1.97 1.97 0 0 0 13 18V6a1.97 1.97 0 0 0-1.934-2Z" />
</svg>
<span class="sr-only">Copy</span>
</button>
<div id="tooltip-copy" role="tooltip"
class="absolute z-10 invisible inline-block w-auto px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
Copy
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
</div>
<button type="button" data-dial-toggle="editor-actions" aria-controls="editor-actions" aria-expanded="false"
class="flex items-center justify-center text-white bg-blue-700 rounded-lg w-14 h-14 hover:bg-blue-800 dark:bg-blue-600 dark:hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 focus:outline-none dark:focus:ring-blue-800">
<i class="ti ti-plus text-3xl transition-transform group-hover:rotate-45"></i>
<span class="sr-only">Open actions menu</span>
</button>
</div>

24
ui/components/meta.njk Normal file
View file

@ -0,0 +1,24 @@
<link rel="icon" type="image/x-icon" href="/img/AppLogo.svg">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/flowbite@2.5.1/dist/flowbite.min.css" rel="stylesheet" />
<!-- <link rel="stylesheet" href="/css/bootstrap.min.css"> -->
<link rel="stylesheet" href="/css/tabler-icons.min.css">
<!-- <script src="/js/jquery-3.6.4.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>-->
<!-- <script src="/js/vue.global.prod.js"></script> -->
<script src="https://unpkg.com/vue@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader"></script>
<style>
body,
html {
/* width: 100svw; */
min-height: 100svh;
user-select: none;
background: var(--bs-body-bg);
}
</style>

50
ui/components/modals.njk Normal file
View file

@ -0,0 +1,50 @@
{% macro insertCreateDirectoryModal() %}
<!-- Modal -->
<div class="modal fade" id="createDirectoryModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Create New Directory</h1>
</div>
<div class="modal-body">
<form id="createDirectoryForm">
<div class="flex-fill">
<label class="form-label">Directory Name:</label>
<input type="text" class="form-control" name="name" placeholder="New Directory">
</div>
</form>
</div>
<div class="modal-footer">
<button id="buttonCancelCreateDirectory" class="btn btn-sm btn-outline-warning px-2" title="save directory" data-bs-toggle="modal" data-bs-target="#createDirectoryModal">
<i class="ti ti-x me-2"></i>Cancel
</button>
<button id="buttonSaveCreateDirectory" class="btn btn-sm btn-outline-primary px-2" title="save directory">
<i class="ti ti-device-floppy me-2"></i>Save
</button>
</div>
</div>
</div>
</div>
<script>
$('#buttonSaveCreateDirectory').on('click', async () => {
console.log("click");
var Response = await fetch(window.location.pathname,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(
Object.fromEntries(
new FormData(document.querySelector('#createDirectoryForm'))
)
),
}).then(() => {
$('#createDirectoryModal').modal('hide');
});
console.log(Response);
})
</script>
{% endmacro %}

110
ui/components/navbar.njk Normal file
View file

@ -0,0 +1,110 @@
<header class="bg-white dark:bg-gray-900 shadow-md sticky top-0 z-50 w-full divide-y divide-gray-300 dark:divide-gray-700 flex flex-col">
<nav class="container flex justify-between items-center py-4 mx-auto relative">
<!-- Application Logo -->
<a href="https://flowbite.com/" class="flex items-center space-x-3 rtl:space-x-reverse z-10">
<img src="https://flowbite.com/docs/images/logo.svg" class="h-6" alt="Flowbite Logo" />
<span class="self-center text-xl font-semibold whitespace-nowrap">Bookmark Manager</span>
</a>
<!-- Quick Search -->
<div class="flex absolute w-full z-0">
<form class="flex items-stretch w-1/4 max-w-sm mx-auto gap-2">
<input type="text" id="simple-search"
class="bg-gray-50 border border-gray-300 text-sm rounded-md focus:border-blue-500 block w-full px-4 py-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400"
placeholder="search bookmarks..." required />
<button type="submit" class="py-1 px-4 text-white bg-blue-700 rounded-md border border-blue-700 hover:bg-blue-800 outline-none dark:bg-blue-600 dark:hover:bg-blue-700">
<i class="ti ti-search"></i>
</button>
</form>
</div>
<!-- User Menu -->
<div class="flex flex-row gap-2 z-10">
<a href="#" class="flex items-center px-4 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800" @click="toggleEditor()">
<i :class="['ti','ti-edit',modeEdit ? 'text-blue-700 dark:text-blue-600' : '']"></i>
</a>
<div class="flex items-center relative md:order-2">
<button type="button" data-dropdown-toggle="user-dropdown"
class="font-medium text-sm px-4 py-2 cursor-pointer rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-white">
Kai Waggeling
</button>
<div id="user-dropdown" class="w-48 hidden my-4 text-base bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600 overflow-hidden absolute right-0">
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">
Settings
</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">
Sign out
</a>
</div>
</div>
</div>
</nav>
<nav class="container flex justify-between items-center py-4 mx-auto relative">
<div class="flex flex-row align-stretch gap-8">
<a href="#" class="block py-1 text-blue-700 dark:text-blue-500">
Start
</a>
<a href="#" class="block py-1 text-gray-900 dark:text-white hover:text-blue-700 dark:hover:text-blue-500">
Bookmarks
</a>
</div>
</nav>
</header>
<!-- <nav class="bg-white border-gray-200 dark:bg-gray-900">
<div class="container flex flex-wrap items-center justify-between mx-auto p-4">
<a href="https://flowbite.com/" class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8" alt="Flowbite Logo" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Bookmark Manager</span>
</a>
<div class="flex items-center md:order-2 space-x-1 md:space-x-0 rtl:space-x-reverse">
<button type="button" data-dropdown-toggle="user-dropdown"
class="inline-flex items-center font-medium justify-center px-4 py-2 text-sm text-gray-900 dark:text-white rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-white">
Kai Waggeling
</button>
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600"
id="user-dropdown">
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a href="#"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">
Settings
</a>
</li>
<li>
<a href="#"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">
Sign out
</a>
</li>
</ul>
</div>
<button data-collapse-toggle="navbar-language" type="button"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
aria-controls="navbar-language" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M1 1h15M1 7h15M1 13h15" />
</svg>
</button>
</div>
<div class="justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-language">
<ul
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<li>
<a href="#"
class="block py-2 px-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500"
aria-current="page">
Start
</a>
</li>
<li>
<a href="#"
class="block py-2 px-3 md:p-0 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700">
Bookmarks
</a>
</li>
</ul>
</div>
</div>
</nav> -->

View file

@ -0,0 +1,37 @@
{% macro jumbotron(id='', title='', text='') %}
<section class="w-full">
<div class="py-4 px-4 mx-auto max-w-screen-xl text-center">
<h1 class="my-4 font-extrabold tracking-tight leading-none text-gray-900 text-4xl md:text-5xl lg:text-6xl">
{{ title }}
</h1>
<p class="my-4 text-lg font-normal text-gray-500 lg:text-xl sm:px-16 lg:px-48">
{{ text }}
</p>
{{ caller() }}
</div>
</section>
{% endmacro %}
{% macro container(id='', width='w-full', flow='row', vAlign='center', hAlign='center') %}
<div class="container mx-auto {{width}} pt-24 pb-32 flex flex-{{flow}} justify-{{hAlign}} items-{{vAlign}}">
{{ caller() }}
</div>
{% endmacro %}
{% macro grid(id='', title='', columns=3) %}
<div class="my-4 grid grid-cols-1 md:grid-cols-{{2 if columns > 2 else columns}} lg:grid-cols-{{columns}} gap-6">
{{ caller() }}
</div>
{% endmacro %}
{% macro link(id='', link='#', title='New Link', description='Description') %}
<a href="{{ link }}" target="_blank" class="p-8 bg-gray-50 rounded-md border border-gray-200 hover:border-purple-400">
<h4 class="font-medium text-gray-700 text-lg mb-4">
{{ title }}
</h4>
<p class="font-normal text-gray-500 text-md">
{{ description }}
</p>
</a>
{% endmacro %}

14
ui/components/widgets.njk Normal file
View file

@ -0,0 +1,14 @@
{% raw %}
<template id="template-jumbotron">
<section class="w-full">
<div class="py-4 px-4 mx-auto max-w-screen-xl text-center">
<h1 class="my-4 font-extrabold tracking-tight leading-none text-gray-900 text-4xl md:text-5xl lg:text-6xl">
{{ title }}
</h1>
<p class="my-4 text-lg font-normal text-gray-500 lg:text-xl sm:px-16 lg:px-48">
{{ text }}
</p>
</div>
</section>
</template>
{% endraw %}

17
ui/error.njk Normal file
View file

@ -0,0 +1,17 @@
{% extends "./master.njk" %}
{% block content %}
<div class="container">
<div class="d-flex align-items-center justify-content-center py-5">
<div class="text-center">
<h1 class="display-1 fw-bold">{{ Error.Code }}</h1>
<p class="fs-3"> <span class="text-danger">Opps!</span> {{ Error.Title }}</p>
<p class="lead">
{{ Error.Message }}
</p>
<a href="{{ Error.Link }}" class="btn btn-primary">Go Back</a>
</div>
</div>
</div>
{% endblock %}

35
ui/master copy.njk Normal file
View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>Shell Script Hub</title>
{% include "./components/meta.njk" %}
<style>
* {
box-sizing: border-box;
}
</style>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark')
}
</script>
</head>
<body class="bg-gray-100 dark:bg-gray-800 dark:text-white">
<div id="app" class="w-full">
{% include "./components/navbar.njk" %}
{% include "./components/editor.njk" %}
<div class="container relative mx-auto py-12 flex flex-col justify-center items-center gap-16">
<dynamic-root :elements="elements"></dynamic-root>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.5.1/dist/flowbite.min.js"></script>
<!-- <script src="/widgets.jumbotron.js"></script> -->
<script src="/start.js"></script>
</body>
</html>

43
ui/master.njk Normal file
View file

@ -0,0 +1,43 @@
{% import "./widgets/jumbotron.njk" as jumbotron %}
{% import "./widgets/title.njk" as title %}
<!DOCTYPE html>
<html lang="de">
<head>
<title>Shell Script Hub</title>
{% include "./components/meta.njk" %}
<style>
* {
box-sizing: border-box;
}
</style>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark')
}
</script>
</head>
<body class="bg-gray-100 dark:bg-gray-800 dark:text-white">
<div id="app" class="w-full flex flex-col gap-12">
{% include "./components/navbar.njk" %}
<div class="container mx-auto relative grid grid-cols-5 gap-4">
<div class="col-span-full row-span-1 border border-gray-500 rounded">{{ jumbotron.createWidget("MTitle", "Message", { center: false }) }}</div>
<div class="col-span-1 row-span-2 border border-gray-500 rounded">2</div>
<div class="col-span-full row-span-1 border border-gray-500 rounded">{{ title.alignLeft("MTitle") }}</div>
<div class="col-span-1 row-span-1 border border-gray-500 rounded">4</div>
<div class="col-span-1 row-span-1 border border-gray-500 rounded">5</div>
<div class="col-span-1 row-span-2 border border-gray-500 rounded">6</div>
<div class="col-span-1 row-span-1 border border-gray-500 rounded">7</div>
<div class="col-span-1 row-span-1 border border-gray-500 rounded">8</div>
<div class="col-span-1 row-span-1 border border-gray-500 rounded">9</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.5.1/dist/flowbite.min.js"></script>
</body>
</html>

31
ui/widgets/jumbotron.njk Normal file
View file

@ -0,0 +1,31 @@
{% macro createWidget(title, text, settings) %}
<section class="flex flex-row py-6 px-8">
{% if settings.center == true %}
<div class="w-full flex flex-col gap-4">
<h1 class="font-extrabold tracking-tight leading-none text-gray-900 text-4xl md:text-5xl lg:text-6xl text-center">
{{ title }}
</h1>
<p class="w-full text-lg font-normal text-gray-500 lg:text-xl mx-2 text-center">
{{ text }}
</p>
{% if ctaSettings and ctaSettings.url %}
<div class="inline-flex flex-row">
<a href="{{ ctaSettings.url }}" class="py-3 px-5 font-medium text-white rounded-lg bg-blue-700 hover:bg-blue-800">
{{ ctaSettings.text }}
</a>
</div>
{% endif %}
</div>
{% else %}
<div class="w-full flex flex-col gap-4">
<h1 class="font-extrabold tracking-tight leading-none text-gray-900 text-4xl md:text-5xl lg:text-6xl">
{{ title }}
</h1>
<p class="text-lg font-normal text-gray-500 lg:text-xl mx-2">
{{ text }}
</p>
</div>
{% endif %}
</section>
{% endmacro %}

16
ui/widgets/title.njk Normal file
View file

@ -0,0 +1,16 @@
{% macro alignLeft(title) %}
<div class="w-full flex flex-col p-4">
<h1 class="font-extrabold text-gray-900 text-4xl md:text-5xl lg:text-6xl text-left pb-4 border-b border-gray-500">
{{ title }}
</h1>
</div>
{% endmacro %}
{% macro largeLeft(title, settings) %}
<div class="w-full flex flex-col py-4">
<h1 class="font-extrabold text-gray-900 text-7xl md:text-8xl lg:text-9xl text-center">
{{ title }}
</h1>
</div>
{% endmacro %}