Enhance security and localization features across the application

- Added rehype-sanitize plugin to the markdown configuration for improved security against XSS attacks.
- Updated environment variables in the codebase to include new configurations for SMTP and monitoring.
- Implemented secure headers in server and Nginx configurations to bolster security.
- Refactored email handling to prevent spoofing by ensuring safe sender addresses.
- Improved localization by updating language persistence and button components for better user experience.
- Enhanced the uptime API and contact form with better error handling and logging practices.
- Updated dependencies in package.json and package-lock.json for better performance and security.
This commit is contained in:
2025-10-19 21:13:15 +02:00
parent 6257a223b2
commit a767dbb115
26 changed files with 4931 additions and 833 deletions

View File

@@ -425,10 +425,10 @@ const metadata = {
{mainT.hero.subtitle}
</div>
<div class="cta-container">
<a href="#about" class="cta-primary-enhanced">
<a href="#about" class="inline-flex items-center justify-center rounded-full font-semibold transition-colors px-6 py-3 text-base bg-blue-600 text-white hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
{t.about.actions.learnMore}
</a>
<a href="#resume" class="cta-secondary-enhanced">
<a href="#resume" class="inline-flex items-center justify-center rounded-full font-semibold transition-colors px-6 py-3 text-base bg-white/90 dark:bg-slate-800 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-700 hover:bg-white dark:hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600">
{t.about.actions.viewExperience}
</a>
</div>

View File

@@ -491,12 +491,12 @@ const metadata = {
text: explosive.actions.learnMore,
href: '#services',
class: 'cta-explosive'
variant: 'primary'
},
{
text: explosive.actions.contactMe,
text: explosive.actions.contactMe,
href: '#contact',
class: 'cta-secondary-explosive'
variant: 'secondary'
},
]}
image={{
@@ -598,7 +598,13 @@ const metadata = {
))}
</div>
<div class="text-lg pt-4 method-highlights" set:html={explosive.approach.highlights.replace(/\n/g, '<br/>')}>
<div class="text-lg pt-4 method-highlights">
{explosive.approach.highlights.split('\n').map((line) => (
<>
{line}
<br />
</>
))}
</div>
</div>
</Fragment>
@@ -612,7 +618,7 @@ const metadata = {
text: explosive.cta.button,
href: '#contact',
icon: 'tabler:rocket',
class: 'cta-explosive'
variant: 'primary'
}}
classes={{
container: 'glass-vibrant'

View File

@@ -0,0 +1,948 @@
---
export const prerender = true;
import Layout from '~/layouts/PageLayout.astro';
import StructuredData from '~/components/common/StructuredData.astro';
import Hero from '~/components/widgets/Hero.astro';
import Content from '~/components/widgets/Content.astro';
import TOPDeskImage from '~/assets/images/topdesk-api-docs.png';
import { getTranslation } from '~/i18n/translations';
import { getTOPDeskDocsTranslation, supportedLanguages } from '~/i18n/translations.topdesk-docs';
import { SITE } from 'astrowind:config';
export async function getStaticPaths() {
return supportedLanguages.map((lang) => ({
params: { lang },
}));
}
const { lang } = Astro.params;
if (!supportedLanguages.includes(lang)) {
return Astro.redirect('/en/topdesk-api-docs');
}
// Get main translations for shared components (navigation, footer, etc.)
const mainT = getTranslation(lang);
// Get TOPDesk docs specific translations
const t = getTOPDeskDocsTranslation(lang);
const metadata = {
title: t.metadata.title,
description: t.metadata.description
};
---
<style>
/* Modern animated background with floating elements - matching aboutme.astro style */
.animated-hero-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-size: 300% 300%;
animation: gradientShift 15s ease infinite;
position: relative;
overflow: hidden;
}
.animated-hero-bg::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px);
background-size: 50px 50px;
animation: backgroundMove 20s linear infinite;
}
.floating-elements {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
}
.floating-shape {
position: absolute;
opacity: 0.15;
animation: float 20s infinite linear;
}
.shape-1 {
top: 20%;
left: 10%;
width: 80px;
height: 80px;
background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0.05));
border-radius: 50%;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
animation-delay: 0s;
}
.shape-2 {
top: 60%;
right: 15%;
width: 120px;
height: 120px;
background: linear-gradient(135deg, rgba(255,255,255,0.15), rgba(255,255,255,0.03));
border-radius: 30px;
backdrop-filter: blur(15px);
border: 1px solid rgba(255,255,255,0.1);
animation-delay: -7s;
}
.shape-3 {
bottom: 30%;
left: 20%;
width: 60px;
height: 60px;
background: linear-gradient(135deg, rgba(255,255,255,0.25), rgba(255,255,255,0.08));
border-radius: 50%;
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.15);
animation-delay: -14s;
}
.shape-4 {
top: 40%;
right: 35%;
width: 40px;
height: 40px;
background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0.05));
border-radius: 8px;
backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.1);
animation-delay: -10s;
}
@keyframes gradientShift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
@keyframes backgroundMove {
0% { transform: translateX(0) translateY(0); }
100% { transform: translateX(-50px) translateY(-50px); }
}
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg) scale(1);
opacity: 0.15;
}
25% {
transform: translateY(-30px) rotate(90deg) scale(1.1);
opacity: 0.25;
}
50% {
transform: translateY(-15px) rotate(180deg) scale(1.05);
opacity: 0.2;
}
75% {
transform: translateY(-25px) rotate(270deg) scale(1.15);
opacity: 0.3;
}
}
/* Enhanced glassmorphism effects for all sections */
:global(.glass-enhanced) {
background: rgba(255, 255, 255, 0.95) !important;
backdrop-filter: blur(20px) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.05) inset !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
:global(.glass-enhanced:hover) {
transform: translateY(-8px) !important;
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(255, 255, 255, 0.1) inset !important;
background: rgba(255, 255, 255, 0.98) !important;
}
/* Dark mode enhancements */
:global(.dark .glass-enhanced) {
background: rgba(30, 30, 50, 0.95) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.03) inset !important;
}
:global(.dark .glass-enhanced:hover) {
background: rgba(35, 35, 60, 0.98) !important;
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.05) inset !important;
}
/* Hero title styling */
.hero-title-enhanced {
font-size: clamp(2.5rem, 6vw, 4.5rem);
font-weight: 900;
background: linear-gradient(135deg,
#667eea 0%,
#764ba2 25%,
#f093fb 50%,
#f5576c 75%,
#4facfe 100%);
background-size: 300% 300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradientShift 8s ease-in-out infinite;
letter-spacing: -0.02em;
line-height: 1.1;
margin-bottom: 1.5rem;
text-shadow: 0 0 30px rgba(102, 126, 234, 0.3);
}
/* Full-width subtitle */
.hero-subtitle-enhanced {
font-size: 1.2rem;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 1.5rem 2rem;
color: #1a1a1a;
width: 100%;
max-width: none;
margin: 0 0 2rem 0;
line-height: 1.6;
font-weight: 500;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
text-align: center;
}
:global(.dark) .hero-subtitle-enhanced {
background: rgba(30, 30, 50, 0.9);
color: #e2e8f0;
border-color: rgba(255, 255, 255, 0.1);
}
/* CTA buttons */
.cta-container {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-top: 1.5rem;
}
.cta-primary-enhanced {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 0.875rem 2rem;
border-radius: 50px;
text-decoration: none;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
border: 2px solid transparent;
position: relative;
overflow: hidden;
}
.cta-primary-enhanced::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.cta-primary-enhanced:hover::before {
left: 100%;
}
.cta-primary-enhanced:hover {
transform: translateY(-5px) scale(1.05);
box-shadow: 0 25px 50px rgba(102, 126, 234, 0.5);
}
/* Documentation content styling - UPDATED FOR FULL WIDTH */
.docs-content {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.7;
color: #333;
width: 100%;
max-width: 75ch;
margin: 0 auto;
padding: 0 1rem;
text-align: left;
}
/* Headings */
.docs-content h2,
.docs-content h3,
.docs-content h4 {
text-align: left;
margin-left: 0;
margin-right: 0;
width: 100%;
}
.docs-content h2 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
margin-top: 40px;
margin-bottom: 20px;
}
.docs-content h3 {
color: #34495e;
border-left: 4px solid #3498db;
padding-left: 15px;
margin-top: 30px;
margin-bottom: 15px;
text-align: left; /* Left align h3 for better readability */
}
.docs-content h4 {
color: #2c3e50;
margin-top: 25px;
margin-bottom: 10px;
text-align: left; /* Left align h4 for better readability */
}
/* Full width content containers - UPDATED */
.content-wrapper {
width: 100%;
max-width: none !important;
margin: 0;
padding: 0;
}
.full-width-content {
width: 100%;
max-width: none !important;
margin: 0 auto;
padding: 0 2rem;
}
.docs-content .mermaid {
text-align: center;
margin: 30px auto;
background: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
width: 100%;
max-width: 100%;
overflow-x: auto;
}
:global(.dark) .docs-content .mermaid {
background: rgba(30, 30, 50, 0.5);
border-color: rgba(255, 255, 255, 0.1);
}
.docs-content pre {
background: #f8f8f8;
border: 1px solid #ddd;
border-radius: 6px;
padding: 15px;
overflow-x: auto;
font-size: 0.9em;
width: 100%;
}
:global(.dark) .docs-content pre {
background: rgba(30, 30, 50, 0.8);
border-color: rgba(255, 255, 255, 0.1);
color: #e2e8f0;
}
.docs-content code {
background: #f1f1f1;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9em;
}
:global(.dark) .docs-content code {
background: rgba(255, 255, 255, 0.1);
color: #e2e8f0;
}
.docs-content table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
:global(.dark) .docs-content table {
background: rgba(30, 30, 50, 0.8);
}
.docs-content th,
.docs-content td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
:global(.dark) .docs-content th,
:global(.dark) .docs-content td {
border-color: rgba(255, 255, 255, 0.1);
}
.docs-content th {
background: #f5f5f5;
font-weight: 600;
color: #333;
}
:global(.dark) .docs-content th {
background: rgba(255, 255, 255, 0.05);
color: #e2e8f0;
}
.alert {
padding: 15px;
margin: 20px 0;
border-radius: 6px;
border-left: 4px solid;
width: 100%;
}
.alert-warning {
background: #fff3cd;
border-left-color: #ffc107;
color: #856404;
}
:global(.dark) .alert-warning {
background: rgba(255, 193, 7, 0.1);
color: #ffc107;
}
.endpoint {
background: #e8f4f8;
border: 1px solid #b8dce8;
border-radius: 6px;
padding: 15px;
margin: 15px 0;
width: 100%;
}
:global(.dark) .endpoint {
background: rgba(30, 30, 50, 0.8);
border-color: rgba(255, 255, 255, 0.1);
}
.endpoint-method {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 0.8em;
margin-right: 10px;
}
.method-get { background: #28a745; color: white; }
.method-post { background: #007bff; color: white; }
.method-patch { background: #ffc107; color: #212529; }
/* Section spacing */
.content-section {
margin: 4rem 0;
position: relative;
width: 100%;
}
.content-section:first-of-type {
margin-top: 2rem;
}
/* Override any container max-width restrictions */
:global(.container) {
max-width: none !important;
width: 100% !important;
}
/* Custom content class overrides */
:global(.content-max-none) {
max-width: none !important;
width: 100% !important;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.hero-subtitle-enhanced {
font-size: 1.1rem;
padding: 1.25rem 1.5rem;
margin: 0 0 1.5rem 0;
}
.cta-container {
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 0 1rem;
}
.cta-primary-enhanced {
width: 100%;
max-width: 260px;
text-align: center;
padding: 0.875rem 1.5rem;
}
.content-section {
margin: 3rem 0;
}
.docs-content,
.full-width-content {
padding: 0 1rem;
}
}
@media (min-width: 769px) {
.docs-content,
.full-width-content {
padding: 0 3rem;
}
}
@media (min-width: 1200px) {
.docs-content,
.full-width-content {
padding: 0 4rem;
}
}
</style>
<Layout metadata={metadata}>
<Fragment slot="announcement"></Fragment>
<!-- Structured Data for SEO -->
<StructuredData
slot="structured-data"
data={{
'@context': 'https://schema.org',
'@type': 'TechnicalArticle',
name: t.metadata.title,
description: t.metadata.description,
author: {
'@type': 'Person',
name: 'Richard Bergsma'
},
datePublished: '2025-08-01',
dateModified: '2025-08-01',
inLanguage: lang
}}
/>
<!-- Enhanced Hero Section -->
<Hero id="hero" isDark={false}>
<Fragment slot="bg">
<div class="animated-hero-bg">
<div class="floating-elements">
<div class="floating-shape shape-1"></div>
<div class="floating-shape shape-2"></div>
<div class="floating-shape shape-3"></div>
<div class="floating-shape shape-4"></div>
</div>
</div>
</Fragment>
<Fragment slot="title">
<span class="hero-title-enhanced">{t.hero.title}</span>
</Fragment>
<Fragment slot="subtitle">
<div class="hero-subtitle-container">
<div class="hero-subtitle-enhanced">
{t.hero.subtitle}
</div>
<div class="cta-container">
<a href="#overview" class="inline-flex items-center justify-center rounded-full font-semibold transition-colors px-6 py-3 text-base bg-blue-600 text-white hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
{t.hero.cta.primary}
</a>
</div>
</div>
</Fragment>
</Hero>
<!-- Documentation Content -->
<div id="overview" class="content-section">
<Content
id="overview"
columns={1}
items={[]}
classes={{
container: 'glass-enhanced content-max-none',
content: 'content-max-none'
}}
>
<Fragment slot="content">
<div class="docs-content">
<h2>{t.sections.overview.title}</h2>
<div class="full-width-content">
<h3>{t.sections.overview.whatIs.title}</h3>
<p>{t.sections.overview.whatIs.content}</p>
<h3>{t.sections.overview.apiOverview.title}</h3>
<ul>
<li><strong>{t.sections.overview.apiOverview.host}:</strong> <code>your-api-host.com</code></li>
<li><strong>{t.sections.overview.apiOverview.basePath}:</strong> <code>/api/v1</code></li>
<li><strong>{t.sections.overview.apiOverview.protocol}:</strong> {t.sections.overview.apiOverview.protocolValue}</li>
<li><strong>{t.sections.overview.apiOverview.authentication}:</strong> {t.sections.overview.apiOverview.authValue}</li>
</ul>
<h3>{t.sections.overview.capabilities.title}</h3>
<ul>
{t.sections.overview.capabilities.items.map((item) => (
<li><strong>{item.title}</strong> {item.description}</li>
))}
</ul>
</div>
</div>
</Fragment>
<Fragment slot="bg">
<div class="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-purple-50/50 dark:from-gray-900/50 dark:to-gray-800/50"></div>
</Fragment>
</Content>
</div>
<!-- Concepts Section -->
<div id="concepts" class="content-section">
<Content
id="concepts"
columns={1}
items={[]}
classes={{
container: 'glass-enhanced content-max-none',
content: 'content-max-none'
}}
>
<Fragment slot="content">
<div class="docs-content">
<h2>{t.sections.concepts.title}</h2>
<div class="full-width-content">
<h3>{t.sections.concepts.entities.title}</h3>
<div class="mermaid" data-mermaid={`flowchart TD
subgraph Users ["👥 Users"]
PERSON[👤 Person<br/>End User]
OPERATOR[👨‍💼 Operator<br/>IT Support Staff]
end
subgraph Teams ["🏢 Organization"]
GROUP[👥 Operator Group<br/>IT Team]
DEPT[🏢 Department]
end
subgraph Tickets ["🎫 Service Requests"]
INCIDENT[🎫 Incident<br/>Support Ticket]
CHANGE[🔄 Change Request<br/>Planned Modification]
end
subgraph Resources ["💼 IT Resources"]
KB[📚 Knowledge Base<br/>Solutions/Procedures]
ASSET[💻 Asset<br/>IT Equipment]
CATEGORY[🏷️ Category<br/>Classification]
end
PERSON -->|reports| INCIDENT
INCIDENT -->|assigned to| OPERATOR
OPERATOR -->|belongs to| GROUP
GROUP -->|manages| CHANGE
OPERATOR -->|uses| KB
INCIDENT -->|classified by| CATEGORY
ASSET -->|linked to| INCIDENT
KB -->|provides solutions for| INCIDENT
classDef user fill:#e3f2fd,stroke:#1976d2,color:#000
classDef team fill:#f3e5f5,stroke:#7b1fa2,color:#000
classDef ticket fill:#fff3e0,stroke:#f57c00,color:#000
classDef resource fill:#e8f5e8,stroke:#388e3c,color:#000
class PERSON,OPERATOR user
class GROUP,DEPT team
class INCIDENT,CHANGE ticket
class KB,ASSET,CATEGORY resource`}>
<!-- Mermaid diagram will be rendered here -->
</div>
<h3>{t.sections.concepts.terminology.title}</h3>
<div class="overflow-x-auto">
<table>
<thead>
<tr>
<th>{t.sections.concepts.terminology.headers.term}</th>
<th>{t.sections.concepts.terminology.headers.description}</th>
<th>{t.sections.concepts.terminology.headers.example}</th>
</tr>
</thead>
<tbody>
{t.sections.concepts.terminology.items.map((item) => (
<tr>
<td><strong>{item.term}</strong></td>
<td>{item.description}</td>
<td>{item.example}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3>{t.sections.concepts.lifecycle.title}</h3>
<div class="mermaid" data-mermaid={`flowchart LR
START[🚀 USER REPORTS<br/>PROBLEM] -->|Creates| NEW[🆕 NEW TICKET<br/>Status: New]
NEW -->|Auto-assigned| ASSIGN[👥 ASSIGNED TO TEAM<br/>Based on category]
ASSIGN -->|Operator picks up| PROGRESS[⚙️ IN PROGRESS<br/>Investigation starts]
PROGRESS -->|Solution found| RESOLVE[✅ RESOLVED<br/>Awaiting confirmation]
RESOLVE -->|User confirms| CLOSE[🔒 CLOSED<br/>Ticket complete]
PROGRESS -->|Need more info| ONHOLD[⏸️ ON HOLD<br/>Waiting for user]
ONHOLD -->|Info received| PROGRESS
classDef start fill:#e8f5e8,stroke:#4caf50,color:#000
classDef active fill:#fff3e0,stroke:#ff9800,color:#000
classDef resolved fill:#e3f2fd,stroke:#2196f3,color:#000
classDef closed fill:#f3e5f5,stroke:#9c27b0,color:#000
classDef hold fill:#fce4ec,stroke:#e91e63,color:#000
class START start
class NEW,ASSIGN,PROGRESS active
class RESOLVE resolved
class CLOSE closed
class ONHOLD hold`}>
<!-- Mermaid diagram will be rendered here -->
</div>
</div>
</div>
</Fragment>
<Fragment slot="bg">
<div class="absolute inset-0 bg-gradient-to-br from-purple-50/50 to-blue-50/50 dark:from-gray-800/50 dark:to-gray-900/50"></div>
</Fragment>
</Content>
</div>
<!-- Getting Started Section -->
<div id="getting-started" class="content-section">
<Content
id="getting-started"
columns={1}
items={[]}
classes={{
container: 'glass-enhanced content-max-none',
content: 'content-max-none'
}}
>
<Fragment slot="content">
<div class="docs-content">
<h2>{t.sections.gettingStarted.title}</h2>
<div class="full-width-content">
<h3>{t.sections.gettingStarted.authentication.title}</h3>
<p>{t.sections.gettingStarted.authentication.description}</p>
<pre><code>{t.sections.gettingStarted.authentication.headers}</code></pre>
<h3>{t.sections.gettingStarted.firstCall.title}</h3>
<p>{t.sections.gettingStarted.firstCall.description}</p>
<pre><code>{t.sections.gettingStarted.firstCall.code}</code></pre>
<h3>{t.sections.gettingStarted.dynamicDropdowns.title}</h3>
<div class="alert alert-warning">
<strong>{t.sections.gettingStarted.dynamicDropdowns.warning}</strong>
</div>
<p>{t.sections.gettingStarted.dynamicDropdowns.explanation}</p>
<h4>{t.sections.gettingStarted.dynamicDropdowns.integration.title}</h4>
<pre><code>{t.sections.gettingStarted.dynamicDropdowns.integration.wrongCode}</code></pre>
<pre><code>{t.sections.gettingStarted.dynamicDropdowns.integration.correctCode}</code></pre>
<h4>{t.sections.gettingStarted.dynamicDropdowns.whyEssential.title}</h4>
<ul>
{t.sections.gettingStarted.dynamicDropdowns.whyEssential.reasons.map((reason) => (
<li><strong>{reason.title}:</strong> {reason.description}</li>
))}
</ul>
</div>
</div>
</Fragment>
<Fragment slot="bg">
<div class="absolute inset-0 bg-gradient-to-br from-green-50/50 to-blue-50/50 dark:from-gray-900/50 dark:to-gray-800/50"></div>
</Fragment>
</Content>
</div>
<!-- API Reference Section -->
<div id="api-reference" class="content-section">
<Content
id="api-reference"
columns={1}
items={[]}
classes={{
container: 'glass-enhanced content-max-none',
content: 'content-max-none'
}}
>
<Fragment slot="content">
<div class="docs-content">
<h2>{t.sections.apiReference.title}</h2>
<div class="full-width-content">
<h3>{t.sections.apiReference.essentialEndpoints.title}</h3>
<h4>{t.sections.apiReference.essentialEndpoints.findUsers.title}</h4>
<div class="endpoint">
<span class="endpoint-method method-get">GET</span>
<code>/api/persons?query=dynamicName=="[Full Name]"</code>
<p><strong>{t.sections.apiReference.essentialEndpoints.findUsers.purpose}:</strong> {t.sections.apiReference.essentialEndpoints.findUsers.purposeText}</p>
<p><strong>{t.sections.apiReference.essentialEndpoints.findUsers.returns}:</strong> {t.sections.apiReference.essentialEndpoints.findUsers.returnsText}</p>
</div>
<h4>{t.sections.apiReference.essentialEndpoints.dropdownOptions.title}</h4>
{t.sections.apiReference.essentialEndpoints.dropdownOptions.endpoints.map((endpoint) => (
<div class="endpoint">
<span class="endpoint-method method-get">GET</span>
<code>{endpoint.path}</code>
<p>{endpoint.description}</p>
</div>
))}
<h4>{t.sections.apiReference.essentialEndpoints.createTicket.title}</h4>
<div class="endpoint">
<span class="endpoint-method method-post">POST</span>
<code>/api/incidents</code>
<p><strong>{t.sections.apiReference.essentialEndpoints.createTicket.purpose}:</strong> {t.sections.apiReference.essentialEndpoints.createTicket.purposeText}</p>
<p><strong>{t.sections.apiReference.essentialEndpoints.createTicket.requires}:</strong> {t.sections.apiReference.essentialEndpoints.createTicket.requiresText}</p>
</div>
<h4>{t.sections.apiReference.essentialEndpoints.searchKnowledgeBase.title}</h4>
<div class="endpoint">
<span class="endpoint-method method-get">GET</span>
<code>/api/knowledgeItems/search?query=[search terms]&lang=en&status=active</code>
<p><strong>{t.sections.apiReference.essentialEndpoints.searchKnowledgeBase.purpose}:</strong> {t.sections.apiReference.essentialEndpoints.searchKnowledgeBase.purposeText}</p>
</div>
</div>
</div>
</Fragment>
<Fragment slot="bg">
<div class="absolute inset-0 bg-gradient-to-br from-orange-50/50 to-red-50/50 dark:from-gray-800/50 dark:to-gray-900/50"></div>
</Fragment>
</Content>
</div>
<!-- Contact Section -->
<div id="contact" class="content-section">
<Content
id="contact"
columns={1}
items={[]}
classes={{
container: 'glass-enhanced content-max-none',
content: 'content-max-none'
}}
>
<Fragment slot="content">
<div class="docs-content">
<div class="full-width-content text-center">
<h2>{t.sections.contact.title}</h2>
<p><strong>{t.sections.contact.team}:</strong> {t.sections.contact.teamName}</p>
<p><strong>{t.sections.contact.email}:</strong> <a href="mailto:support@example.com">support@example.com</a></p>
<p><strong>{t.sections.contact.website}:</strong> <a href="#" target="_blank">www.example.com</a></p>
<p>{t.sections.contact.supportText}</p>
</div>
</div>
</Fragment>
<Fragment slot="bg">
<div class="absolute inset-0 bg-gradient-to-br from-indigo-50/50 to-purple-50/50 dark:from-gray-900/50 dark:to-gray-800/50"></div>
</Fragment>
</Content>
</div>
</Layout>
<script>
// Initialize Mermaid diagrams with proper client-side setup
document.addEventListener('DOMContentLoaded', async () => {
try {
// Import Mermaid dynamically from local dependency (no remote CDN)
const mermaidModule = await import('mermaid');
const mermaid = mermaidModule.default || mermaidModule;
// Initialize Mermaid with configuration
mermaid.initialize({
startOnLoad: false,
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default',
flowchart: {
useMaxWidth: true,
htmlLabels: true,
curve: 'basis'
},
themeVariables: {
fontFamily: 'Arial, sans-serif'
}
});
// Find all mermaid elements and render them
const mermaidElements = document.querySelectorAll('.mermaid');
for (let i = 0; i < mermaidElements.length; i++) {
const element = mermaidElements[i];
const mermaidCode = element.textContent.trim();
try {
// Create a unique ID for each diagram
const id = `mermaid-${i}`;
// Render the diagram
const { svg } = await mermaid.render(id, mermaidCode);
// Replace the element content with the rendered SVG
element.innerHTML = svg;
} catch (error) {
console.error('Error rendering mermaid diagram:', error);
element.innerHTML = `<div style="color: red; padding: 20px; border: 1px solid red; border-radius: 4px;">
<strong>Diagram Error:</strong> Unable to render diagram. Please check the syntax.
</div>`;
}
}
} catch (error) {
console.error('Error loading Mermaid:', error);
}
});
// Re-render mermaid diagrams when theme changes or page loads
document.addEventListener('astro:page-load', () => {
// Trigger re-initialization if needed
setTimeout(() => {
const event = new Event('DOMContentLoaded');
document.dispatchEvent(event);
}, 100);
});
// Theme change handler
const themeToggle = document.querySelector('[data-aw-toggle-color-scheme]');
if (themeToggle) {
themeToggle.addEventListener('click', () => {
setTimeout(() => {
const event = new Event('DOMContentLoaded');
document.dispatchEvent(event);
}, 200);
});
}
</script>

View File

@@ -13,7 +13,7 @@ export const GET: APIRoute = async ({ request }) => {
headers['Authorization'] = `token ${import.meta.env.GITEA_TOKEN}`;
}
const response = await fetch(url, { headers });
const response = await fetch(url, { headers, redirect: 'follow', cache: 'no-store' });
if (!response.ok) {
throw new Error(`Gitea API responded with status: ${response.status}`);

View File

@@ -5,13 +5,11 @@ import {
checkRateLimit,
sendAdminNotification,
sendUserConfirmation,
sendEmail,
} from '../../utils/email-handler';
import { isSpamWithGemini } from "../../utils/gemini-spam-check";
import jwt from 'jsonwebtoken';
const MANUAL_REVIEW_SECRET = process.env.MANUAL_REVIEW_SECRET;
const MANUAL_REVIEW_EMAIL = 'manual-review@365devnet.eu';
// Enhanced email validation with more comprehensive regex
const isValidEmail = (email: string): boolean => {
@@ -128,18 +126,18 @@ export const GET: APIRoute = async ({ request }) => {
export const POST: APIRoute = async ({ request, clientAddress }) => {
try {
console.log('Contact form submission received');
// Minimal server logs; avoid PII in logs
// Get client IP address for rate limiting
const ipAddress = clientAddress || '0.0.0.0';
console.log('Client IP:', ipAddress);
// Do not log full IP or headers in production
// Check rate limit
const rateLimitCheck = await checkRateLimit(ipAddress);
console.log('Rate limit check:', rateLimitCheck);
// Log only when exceeded (and without PII)
if (rateLimitCheck.limited) {
console.log('Rate limit exceeded');
console.warn('Rate limit exceeded for contact endpoint');
return new Response(
JSON.stringify({
success: false,
@@ -159,10 +157,10 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
// Get form data
const formData = await request.formData();
console.log('Form data received');
// Form data received
// Log all form data keys
console.log('Form data keys:', [...formData.keys()]);
// Don't log field values
const name = formData.get('name')?.toString() || '';
const email = formData.get('email')?.toString() || '';
@@ -171,14 +169,7 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
const csrfToken = formData.get('csrf_token')?.toString() || '';
const domain = formData.get('domain')?.toString() || '';
console.log('Form data values:', {
name,
email,
messageLength: message.length,
disclaimer,
csrfToken: csrfToken ? 'present' : 'missing',
domain,
});
// Avoid logging PII; capture minimal telemetry only if needed
// Get user agent for logging and spam detection
const userAgent = request.headers.get('user-agent') || 'Unknown';
@@ -224,10 +215,10 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
spamReason = 'heuristic';
}
if (spamDetected) {
const token = jwt.sign({ email, message }, MANUAL_REVIEW_SECRET, { expiresIn: '1h' });
const secret = MANUAL_REVIEW_SECRET || '';
const token = secret ? jwt.sign({ email, message }, secret, { expiresIn: '1h' }) : '';
console.warn(
`[SPAM DETECTED by ${spamReason === 'Gemini' ? 'Gemini' : 'heuristic'}]`,
{ name, email, message, ip: request.headers.get('x-forwarded-for') }
`[SPAM DETECTED by ${spamReason === 'Gemini' ? 'Gemini' : 'heuristic'}]`
);
return new Response(
JSON.stringify({
@@ -256,13 +247,13 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
}
// Send emails
console.log('Attempting to send admin notification email');
// Send admin notification email
const adminEmailSent = await sendAdminNotification(name, email, message, ipAddress, userAgent, domain);
console.log('Admin email sent result:', adminEmailSent);
console.log('Attempting to send user confirmation email');
// Send user confirmation email
const userEmailSent = await sendUserConfirmation(name, email, message, domain);
console.log('User email sent result:', userEmailSent);
// Check if emails were sent successfully
if (!adminEmailSent || !userEmailSent) {

View File

@@ -89,7 +89,7 @@ function ensureUTC(dateString: string): string {
export const GET: APIRoute = async () => {
try {
if (!UPTIME_KUMA_URL) {
console.error('Missing environment variable: UPTIME_KUMA_URL');
// Avoid noisy logs in production; return typed error instead
return new Response(JSON.stringify({ error: 'Configuration error: Missing environment variable' }), {
status: 500,
headers: {
@@ -191,10 +191,10 @@ export const GET: APIRoute = async () => {
},
});
} catch (err) {
console.error('Error fetching uptime data:', err);
// Log minimal error context without leaking internals
return new Response(JSON.stringify({
error: 'Failed to fetch uptime data',
details: err instanceof Error ? err.message : 'Unknown error'
details: 'upstream error'
}), {
status: 500,
headers: {