commit 651d2f8dfc0ab0448d31c45de6d91c50841c4947 Author: ronin Date: Sun Mar 29 17:58:06 2026 +0000 Initial commit — migrate existing site diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..c21b3bb --- /dev/null +++ b/admin.html @@ -0,0 +1,758 @@ + + + + + + Admin — Rawand Lorentzen + + + + + + + + + +
+ +
+ + +
+ + +
+ + + + +
+ + +
+
+ + + +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+ + +
+ + +
+
+ + + +
+
+
+
+
+
+ + +
+
+
+ + +
+ + +
+
+ + + +
+
+
+
+ + +
+
+ + + +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+ + +
+ + +
+
+ + + +
+
+
+
+
+
+ + +
+
+
+ + +
+ + +
+
+ + + +
+
+
+
+ + +
+
+ + + +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ + +
+ + +
+
+ + + +
+
+
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ + +
+ + +
+
+ + + +
+
+
+
+ + + +
+ + + + + diff --git a/api/config.json b/api/config.json new file mode 100644 index 0000000..de7f069 --- /dev/null +++ b/api/config.json @@ -0,0 +1,4 @@ +{ + "username": "ronin", + "passwordHash": "8518f2bf8cc5ffc6fcf27f52fc6532641260982d1c6bfb05c03e4002dc421a00" +} diff --git a/api/links.json b/api/links.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/api/links.json @@ -0,0 +1 @@ +[] diff --git a/api/notes.json b/api/notes.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/api/notes.json @@ -0,0 +1 @@ +[] diff --git a/api/posts.json b/api/posts.json new file mode 100644 index 0000000..c18bf62 --- /dev/null +++ b/api/posts.json @@ -0,0 +1,26 @@ +[ + { + "slug": "setting-up-forgejo-on-hetzner", + "title": "Setting up a self-hosted Forgejo on Hetzner from scratch", + "content": "", + "date": "2026-03-29" + }, + { + "slug": "cis-compliance-scoring-terraform", + "title": "CIS IMP2 compliance scoring with Terraform static analysis", + "content": "", + "date": "2026-03-15" + }, + { + "slug": "databricks-workspace-entra-id", + "title": "Databricks workspace provisioning with Entra ID groups", + "content": "", + "date": "2026-02-28" + }, + { + "slug": "azure-landing-zone-terraform", + "title": "Azure Landing Zone — a complete Terraform implementation guide", + "content": "", + "date": "2026-02-10" + } +] diff --git a/api/projects.json b/api/projects.json new file mode 100644 index 0000000..afc8dfa --- /dev/null +++ b/api/projects.json @@ -0,0 +1,58 @@ +[ + { + "slug": "azure-landing-zone", + "title": "Azure Landing Zone", + "description": "Contributed to a full Landing Zone implementation using Terraform at CIMT. Covering governance, policy, networking and RBAC across Azure environments.", + "url": "", + "tags": "Terraform, Azure, Governance, RBAC", + "date": "2026-03-29" + }, + { + "slug": "dap-data-access-platform", + "title": "DAP — Data Access Platform", + "description": "Worked on Databricks workspace provisioning with Entra ID group-based access control and ADLS Gen2 integration at CIMT.", + "url": "", + "tags": "Databricks, Entra ID, Terraform, ADLS Gen2", + "date": "2026-03-29" + }, + { + "slug": "cis-compliance-checker", + "title": "CIS Compliance Checker", + "description": "Contributed to a Python static compliance checker for Terraform files, comparing current vs predicted CIS IMP2 scores across Azure projects.", + "url": "", + "tags": "Python, CIS Controls, Terraform, OOP", + "date": "2026-03-29" + }, + { + "slug": "self-hosted-forge", + "title": "Self-Hosted Forge", + "description": "Personal portfolio and self-hosted platform built from scratch. Running on Hetzner with Forgejo, Nginx reverse proxy, Let's Encrypt SSL and Docker Compose.", + "url": "", + "tags": "Forgejo, Docker, Nginx, Hetzner, Linux", + "date": "2026-03-29" + }, + { + "slug": "fingerprint-pill-dispenser", + "title": "Fingerprint Pill Dispenser", + "description": "A proof of concept exploring embedded systems and access control. Built on an ESP32 microcontroller with an AS608 fingerprint sensor — patients register their fingerprint, which must be verified before a pill dispenser unlocks. The dispenser was a physical enclosure controlled by an MG90S servo motor. Developed in Python with object-oriented design.", + "url": "", + "tags": "ESP32, AS608, Python, OOP, Embedded, MG90S Servo", + "date": "2026-03-29" + }, + { + "slug": "this-website", + "title": "This Website", + "description": "Personal portfolio site, self-hosted on Hetzner. Built from scratch with HTML, CSS and JavaScript — no frameworks, no dependencies.", + "url": "", + "tags": "HTML/CSS, JavaScript, Nginx, Hetzner", + "date": "2026-03-29" + }, + { + "slug": "postgresql-database", + "title": "PostgreSQL Database", + "description": "Set up and administered a self-hosted PostgreSQL database as part of personal infrastructure. Includes schema design and integration with hosted services.", + "url": "", + "tags": "PostgreSQL, Linux, Docker", + "date": "2026-03-29" + } +] diff --git a/api/server.js b/api/server.js new file mode 100644 index 0000000..ca3c994 --- /dev/null +++ b/api/server.js @@ -0,0 +1,247 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const url = require('url'); + +const PORT = 4000; +const CONFIG_FILE = path.join(__dirname, 'config.json'); + +const DATA_FILES = { + blog: path.join(__dirname, 'posts.json'), + notes: path.join(__dirname, 'notes.json'), + projects: path.join(__dirname, 'projects.json'), + links: path.join(__dirname, 'links.json'), +}; + +const sessions = new Map(); + +function loadConfig() { + if (!fs.existsSync(CONFIG_FILE)) { + const config = { username: 'admin', passwordHash: crypto.createHash('sha256').update('admin').digest('hex') }; + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); + return config; + } + return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); +} + +function loadData(type) { + const file = DATA_FILES[type]; + if (!fs.existsSync(file)) return []; + return JSON.parse(fs.readFileSync(file, 'utf8')); +} + +function saveData(type, items) { + fs.writeFileSync(DATA_FILES[type], JSON.stringify(items, null, 2)); +} + +function generateToken() { + return crypto.randomBytes(32).toString('hex'); +} + +function isAuthenticated(req) { + const auth = req.headers['authorization'] || ''; + const token = auth.replace('Bearer ', '').trim(); + if (!token) return false; + const expiry = sessions.get(token); + if (!expiry || Date.now() > expiry) { + sessions.delete(token); + return false; + } + return true; +} + +function readBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', () => resolve(body)); + req.on('error', reject); + }); +} + +function slugify(title) { + return title.toLowerCase() + .replace(/æ/g, 'ae').replace(/ø/g, 'oe').replace(/å/g, 'aa') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); +} + +function uniqueSlug(base, items) { + if (!items.find(p => p.slug === base)) return base; + let i = 2; + while (items.find(p => p.slug === `${base}-${i}`)) i++; + return `${base}-${i}`; +} + +const server = http.createServer(async (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader('Content-Type', 'application/json'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + const parsed = url.parse(req.url, true); + const pathname = parsed.pathname; + + // POST /api/auth/login + if (req.method === 'POST' && pathname === '/api/auth/login') { + try { + const body = await readBody(req); + const { username, password } = JSON.parse(body); + const config = loadConfig(); + const hash = crypto.createHash('sha256').update(password || '').digest('hex'); + if (username === config.username && hash === config.passwordHash) { + const token = generateToken(); + sessions.set(token, Date.now() + 24 * 60 * 60 * 1000); + res.writeHead(200); + res.end(JSON.stringify({ token })); + } else { + res.writeHead(401); + res.end(JSON.stringify({ error: 'Forkert brugernavn eller adgangskode' })); + } + } catch { + res.writeHead(400); + res.end(JSON.stringify({ error: 'Bad request' })); + } + return; + } + + // POST /api/auth/logout + if (req.method === 'POST' && pathname === '/api/auth/logout') { + const auth = req.headers['authorization'] || ''; + const token = auth.replace('Bearer ', '').trim(); + sessions.delete(token); + res.writeHead(200); + res.end(JSON.stringify({ ok: true })); + return; + } + + const listMatch = pathname.match(/^\/api\/(blog|notes|projects|links)$/); + const itemMatch = pathname.match(/^\/api\/(blog|notes|projects|links)\/([^/]+)$/); + + // GET list + if (req.method === 'GET' && listMatch) { + const type = listMatch[1]; + const items = loadData(type); + if (type === 'blog') { + res.writeHead(200); + res.end(JSON.stringify(items.map(({ slug, title, date }) => ({ slug, title, date })))); + } else { + res.writeHead(200); + res.end(JSON.stringify(items)); + } + return; + } + + // GET single item + if (req.method === 'GET' && itemMatch) { + const [, type, slug] = itemMatch; + const item = loadData(type).find(p => p.slug === slug); + if (!item) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; } + res.writeHead(200); + res.end(JSON.stringify(item)); + return; + } + + // POST create + if (req.method === 'POST' && listMatch) { + const type = listMatch[1]; + if (!isAuthenticated(req)) { res.writeHead(401); res.end(JSON.stringify({ error: 'Unauthorized' })); return; } + try { + const body = await readBody(req); + const data = JSON.parse(body); + const items = loadData(type); + const today = new Date().toISOString().split('T')[0]; + let item; + + if (type === 'blog' || type === 'notes') { + const { title, content } = data; + if (!title || !content) { res.writeHead(400); res.end(JSON.stringify({ error: 'Titel og indhold er påkrævet' })); return; } + const slug = uniqueSlug(slugify(title), items); + item = { slug, title, content, date: today }; + } else if (type === 'projects') { + const { title, description, url: projectUrl, tags } = data; + if (!title) { res.writeHead(400); res.end(JSON.stringify({ error: 'Titel er påkrævet' })); return; } + const slug = uniqueSlug(slugify(title), items); + item = { slug, title, description: description || '', url: projectUrl || '', tags: tags || '', date: today }; + } else if (type === 'links') { + const { title, url: linkUrl, description, category } = data; + if (!title || !linkUrl) { res.writeHead(400); res.end(JSON.stringify({ error: 'Titel og URL er påkrævet' })); return; } + const slug = uniqueSlug(slugify(title), items); + item = { slug, title, url: linkUrl, description: description || '', category: category || '', date: today }; + } + + items.unshift(item); + saveData(type, items); + res.writeHead(201); + res.end(JSON.stringify(item)); + } catch { + res.writeHead(400); + res.end(JSON.stringify({ error: 'Bad request' })); + } + return; + } + + // PUT update + if (req.method === 'PUT' && itemMatch) { + const [, type, slug] = itemMatch; + if (!isAuthenticated(req)) { res.writeHead(401); res.end(JSON.stringify({ error: 'Unauthorized' })); return; } + try { + const body = await readBody(req); + const data = JSON.parse(body); + const items = loadData(type); + const idx = items.findIndex(p => p.slug === slug); + if (idx === -1) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; } + + if (type === 'blog' || type === 'notes') { + if (data.title !== undefined) items[idx].title = data.title; + if (data.content !== undefined) items[idx].content = data.content; + } else if (type === 'projects') { + if (data.title !== undefined) items[idx].title = data.title; + if (data.description !== undefined) items[idx].description = data.description; + if (data.url !== undefined) items[idx].url = data.url; + if (data.tags !== undefined) items[idx].tags = data.tags; + } else if (type === 'links') { + if (data.title !== undefined) items[idx].title = data.title; + if (data.url !== undefined) items[idx].url = data.url; + if (data.description !== undefined) items[idx].description = data.description; + if (data.category !== undefined) items[idx].category = data.category; + } + + saveData(type, items); + res.writeHead(200); + res.end(JSON.stringify(items[idx])); + } catch { + res.writeHead(400); + res.end(JSON.stringify({ error: 'Bad request' })); + } + return; + } + + // DELETE + if (req.method === 'DELETE' && itemMatch) { + const [, type, slug] = itemMatch; + if (!isAuthenticated(req)) { res.writeHead(401); res.end(JSON.stringify({ error: 'Unauthorized' })); return; } + const items = loadData(type); + const idx = items.findIndex(p => p.slug === slug); + if (idx === -1) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; } + items.splice(idx, 1); + saveData(type, items); + res.writeHead(200); + res.end(JSON.stringify({ ok: true })); + return; + } + + res.writeHead(404); + res.end(JSON.stringify({ error: 'Not found' })); +}); + +server.listen(PORT, '127.0.0.1', () => { + console.log(`API listening on port ${PORT}`); +}); diff --git a/blog-post.html b/blog-post.html new file mode 100644 index 0000000..49b0c27 --- /dev/null +++ b/blog-post.html @@ -0,0 +1,171 @@ + + + + + + Blog — Rawand Lorentzen + + + + + + + + +
+
+
...
+

Indlæser...

+
+
+ + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..c15c05d --- /dev/null +++ b/index.html @@ -0,0 +1,787 @@ + + + + + + Rawand Lorentzen + + + + + + +
+
+ + + + +
+
+
IT-Teknolog — Cloud & Infrastructure
+

Rawand Lorentzen

+
// terraform apply && git push
+

+ Cloud engineer with a background in network protocols, + embedded systems and agile project work. + Working with Azure infrastructure and IaC in production — documenting everything along the way. +

+ +
+ +
+
+
+
+
+ rawand@hetzner ~ $ +
+
+
whoami
+
rawand_lorentzen
+
+
cat certs.txt
+
→ IT-Teknolog (Graduate)
+
→ CompTIA Network+
+
→ AZ-104
+
→ AZ-500 (in progress)
+
+
cat stack.txt
+
→ Azure / Terraform / IaC
+
→ OSPF / BGP / TCP-IP
+
→ Python / OOP
+
→ Docker / Kubernetes
+
→ Databricks / Power BI
+
+
+
+
+
+ +
+ + +
+
+
01 — About
+

Who I am

+

Engineer, problem solver, lifelong learner.

+
+
+
+

+ Rawand Lorentzen is a qualified IT-Teknolog with a broad technical foundation spanning cloud infrastructure, network engineering, and software development. His education covered the full stack of modern IT — from low-level routing protocols like OSPF and BGP, communication architectures, and TCP/IP networking, to object-oriented Python programming in the context of embedded systems. +

+

+ Since transitioning into cloud infrastructure, Rawand has gained hands-on experience with Microsoft Azure and Terraform-based Infrastructure as Code (IaC) — working in production environments during his internship at CIMT across governance, CI/CD pipelines, Defender for Cloud, and CIS compliance frameworks. He holds the AZ-104 certification and is currently working toward AZ-500. +

+

+ On the data side, he has worked with Databricks and Power BI — including workspace provisioning, access control, and integrating data platforms into cloud infrastructure. He has also worked with Docker and Kubernetes, gaining practical experience with containerised workloads in cloud-native environments. +

+

+ Before IT, Rawand spent close to a decade in physical craftsmanship. That background shaped a mindset that carries directly into infrastructure work: methodical, detail-oriented, and always built to last. +

+
+ Terraform + Azure + IaC + OSPF / BGP + TCP/IP + Python + OOP + Embedded Systems + Docker + Kubernetes + Linux + CI/CD + CIS Controls + Agile / Scrum + Databricks + Power BI + AZ-104 + AZ-500 +
+
+
+
+ +
+ + +
+
+
02 — Git
+

Public repositories

+

Live from Forgejo — browse and explore.

+
+
+
Loading repositories from Forgejo...
+
+
+ +
+ + +
+
+
03 — Projects
+

What I've worked on

+

A mix of internship work and personal projects.

+
+
+ +
+
+
001
+ Internship — CIMT +
+
Azure Landing Zone
+

Contributed to a full Landing Zone implementation using Terraform at CIMT. Covering governance, policy, networking and RBAC across Azure environments.

+
+ Terraform + Azure + Governance + RBAC +
+
+ +
+
+
002
+ Internship — CIMT +
+
DAP — Data Access Platform
+

Worked on Databricks workspace provisioning with Entra ID group-based access control and ADLS Gen2 integration at CIMT.

+
+ Databricks + Entra ID + Terraform + ADLS Gen2 +
+
+ +
+
+
003
+ Internship — CIMT +
+
CIS Compliance Checker
+

Contributed to a Python static compliance checker for Terraform files, comparing current vs predicted CIS IMP2 scores across Azure projects.

+
+ Python + CIS Controls + Terraform + OOP +
+
+ +
+
+
004
+ Personal +
+
Self-Hosted Forge
+

Personal portfolio and self-hosted platform built from scratch. Running on Hetzner with Forgejo, Nginx reverse proxy, Let's Encrypt SSL and Docker Compose.

+
+ Forgejo + Docker + Nginx + Hetzner + Linux +
+
+ +
+
+
005
+ School Project +
+
Fingerprint Pill Dispenser
+

A proof of concept exploring embedded systems and access control. Built on an ESP32 microcontroller with an AS608 fingerprint sensor — patients register their fingerprint, which must be verified before a pill dispenser unlocks. The dispenser was a physical enclosure controlled by an MG90S servo motor. Developed in Python with object-oriented design.

+
+ ESP32 + AS608 + Python + OOP + Embedded + MG90S Servo +
+
+ +
+
+
006
+ Personal +
+
This Website
+

Personal portfolio site, self-hosted on Hetzner. Built from scratch with HTML, CSS and JavaScript — no frameworks, no dependencies.

+
+ HTML/CSS + JavaScript + Nginx + Hetzner +
+
+ +
+
+
007
+ Personal +
+
PostgreSQL Database
+

Set up and administered a self-hosted PostgreSQL database as part of personal infrastructure. Includes schema design and integration with hosted services.

+
+ PostgreSQL + Linux + Docker +
+
+ +
+
+ +
+ + +
+
+
04 — Blog
+

Articles & writeups

+

Technical deep-dives and lessons learned.

+
+
+
Indlæser...
+
+
+ +
+ + + + + + + + +