initial upload
This commit is contained in:
parent
b2a512f7fe
commit
48ae5e89aa
30 changed files with 1293 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
logs/
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
6
assets/css/bootstrap.min.css
vendored
Normal file
6
assets/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/css/bootstrap.min.css.map
Normal file
1
assets/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
4
assets/css/tabler-icons.min.css
vendored
Normal file
4
assets/css/tabler-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/fonts/tabler-icons.eot
Normal file
BIN
assets/fonts/tabler-icons.eot
Normal file
Binary file not shown.
BIN
assets/fonts/tabler-icons.ttf
Normal file
BIN
assets/fonts/tabler-icons.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/tabler-icons.woff
Normal file
BIN
assets/fonts/tabler-icons.woff
Normal file
Binary file not shown.
BIN
assets/fonts/tabler-icons.woff2
Normal file
BIN
assets/fonts/tabler-icons.woff2
Normal file
Binary file not shown.
1
assets/js/vue.global.prod.js
Normal file
1
assets/js/vue.global.prod.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/js/vue.runtime.global.prod.js
Normal file
1
assets/js/vue.runtime.global.prod.js
Normal file
File diff suppressed because one or more lines are too long
32
datastore/certificates/1a278ace.crt
Normal file
32
datastore/certificates/1a278ace.crt
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
1
datastore/database/authorityDB.json
Normal file
1
datastore/database/authorityDB.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
6
datastore/database/certificates.json
Normal file
6
datastore/database/certificates.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"1a278ace": {
|
||||||
|
"displayName": "waggeling.net rootCA",
|
||||||
|
"certificateType": "rootCA"
|
||||||
|
}
|
||||||
|
}
|
||||||
51
datastore/keys/1a278ace.key
Normal file
51
datastore/keys/1a278ace.key
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
58
functions/db.authorities.mjs
Normal file
58
functions/db.authorities.mjs
Normal 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}"})
|
||||||
91
functions/db.certificates.mjs
Normal file
91
functions/db.certificates.mjs
Normal 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
134
functions/db.users.mjs
Normal 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}`);
|
||||||
|
}
|
||||||
84
functions/pki.createCA.mjs
Normal file
84
functions/pki.createCA.mjs
Normal 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();
|
||||||
77
functions/pki.createCSR.mjs
Normal file
77
functions/pki.createCSR.mjs
Normal 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();
|
||||||
298
functions/pki.createCert.mjs
Normal file
298
functions/pki.createCert.mjs
Normal 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
51
functions/pki.utils.mjs
Normal 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
92
functions/structure.mjs
Normal 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;
|
||||||
|
}
|
||||||
37
master.mjs
Normal file
37
master.mjs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
// Load WebServer
|
||||||
|
import express from "express";
|
||||||
|
import { router } from "express-file-routing"
|
||||||
|
|
||||||
|
// Load Templating Engine
|
||||||
|
import Nunjucks from "nunjucks";
|
||||||
|
|
||||||
|
// Initialize WebServer
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
// Middleware, um CORS zu aktivieren
|
||||||
|
app.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: app,
|
||||||
|
noCache: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mount Middlewares to WebServer
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded());
|
||||||
|
app.use(express.static('./assets/'))
|
||||||
|
app.use("/", await router());
|
||||||
|
|
||||||
|
// Server starten
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server is listening on port ${port}`);
|
||||||
|
});
|
||||||
21
package.json
Normal file
21
package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"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": "^4.21.2",
|
||||||
|
"express-file-routing": "^3.0.3",
|
||||||
|
"node-forge": "^1.3.1",
|
||||||
|
"node-json-db": "^2.3.1",
|
||||||
|
"nunjucks": "^3.2.4",
|
||||||
|
"parse-duration": "^2.1.3",
|
||||||
|
"validate": "^5.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
routes/authorities.mjs
Normal file
12
routes/authorities.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
export const get = async (request, response) => {
|
||||||
|
if (request.method !== "GET") return response.status(405)
|
||||||
|
|
||||||
|
response.render(`ui/master.njk`, {
|
||||||
|
page: 'authorities',
|
||||||
|
userInfo: {
|
||||||
|
firstName: 'Kai Uwe',
|
||||||
|
lastName: 'Waggeling'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
7
test.mjs
Normal file
7
test.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAllCertificates
|
||||||
|
} from "./functions/db.certificates.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
console.log((await getAllCertificates()));
|
||||||
24
ui/components/meta.njk
Normal file
24
ui/components/meta.njk
Normal 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>
|
||||||
101
ui/components/navbar.njk
Normal file
101
ui/components/navbar.njk
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<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">Private Key Infrastructure</span>
|
||||||
|
</a>
|
||||||
|
<!-- 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>
|
||||||
|
{% if page == 'authority' %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
</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> -->
|
||||||
36
ui/master.njk
Normal file
36
ui/master.njk
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!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" %}
|
||||||
|
<div class="container relative mx-auto py-12 flex flex-col justify-center items-center gap-16">
|
||||||
|
{% if page == 'authorities' %}
|
||||||
|
{% include "./pages/authorities.njk" %}
|
||||||
|
{% endif %}
|
||||||
|
</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>
|
||||||
64
ui/pages/authorities.njk
Normal file
64
ui/pages/authorities.njk
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<section class="antialiased w-full">
|
||||||
|
<div class="mx-auto max-w-screen-xl px-4 2xl:px-0">
|
||||||
|
<!-- Heading & Filters -->
|
||||||
|
<div class="mb-4 items-end justify-between space-y-4 sm:flex sm:space-y-0 md:mb-8">
|
||||||
|
<div>
|
||||||
|
<nav class="inline-flex items-center gap-2" aria-label="Breadcrumb">
|
||||||
|
<svg class="me-2.5 h-3 w-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="m19.707 9.293-2-2-7-7a1 1 0 0 0-1.414 0l-7 7-2 2a1 1 0 0 0 1.414 1.414L2 10.414V18a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 0 1 1h3a2 2 0 0 0 2-2v-7.586l.293.293a1 1 0 0 0 1.414-1.414Z" />
|
||||||
|
</svg>
|
||||||
|
<a href="#" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-primary-600 dark:text-gray-400 dark:hover:text-white">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
<svg class="h-5 w-5 text-gray-400 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 5 7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
<a href="#" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-primary-600 dark:text-gray-400 dark:hover:text-white">
|
||||||
|
Certificate Authorities
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<h2 class="mt-3 text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">Certificate Authorities</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col grid gap-4">
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-700">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<a href="#" class="text-lg font-semibold leading-tight text-gray-900 hover:underline dark:text-white">
|
||||||
|
MVZ Labor Dr. Reising-Ackermann & Kollegen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="me-2 rounded bg-red-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-red-100 dark:text-gray-300">
|
||||||
|
Expires: 01.06.2025
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ul class="mt-2 flex items-center gap-4">
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<svg class="h-4 w-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h6l2 4m-8-4v8m0-8V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v9h2m8 0H9m4 0h2m4 0h2v-4m0 0h-5m3.5 5.5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Zm-10 0a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Fast Delivery</p>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<svg class="h-4 w-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M8 7V6c0-.6.4-1 1-1h11c.6 0 1 .4 1 1v7c0 .6-.4 1-1 1h-1M3 18v-7c0-.6.4-1 1-1h11c.6 0 1 .4 1 1v7c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1Zm8-3.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Best Price</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
|
<p class="text-2xl font-extrabold leading-tight text-gray-900 dark:text-white">$1,699</p>
|
||||||
|
|
||||||
|
<button type="button" class="inline-flex items-center rounded-lg bg-primary-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
|
||||||
|
<svg class="-ms-2 me-2 h-5 w-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4h1.5L8 16m0 0h8m-8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm.75-3H7.5M11 7H6.312M17 4v6m-3-3h6" />
|
||||||
|
</svg>
|
||||||
|
Add to cart
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue