Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ freelancer.instructions.md
/lib/generated/prisma
# Private Notes
notes.md
desktop.ini
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ Comprehensive API endpoints with security-first design:
| `/api/blog`       | Blog content management and retrieval             | Prisma + Zod                       |
| `/api/github`     | Fetches GitHub profile + repos (filtered)         | Tokenized (env)                   |
| `/api/pagespeed` | Surfaces PageSpeed metrics                         | Enhanced caching + error handling |
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /api/pagespeed endpoint is still listed in the API surface table, but this endpoint has been removed in this PR. This entry should be deleted to match the actual API structure.

Suggested change
| `/api/pagespeed` | Surfaces PageSpeed metrics                         | Enhanced caching + error handling |

Copilot uses AI. Check for mistakes.
| `/api/chatbot`   | Interactive AI chatbot (Reem) for visitor queries | Gemini + Groq fallback             |
| `/api/chatbot`   | Interactive AI chatbot (Reem) for visitor queries | Groq API           |
| `/api/admin`     | Administrative operations for content             | Secured endpoints                 |

Controls:
Expand Down Expand Up @@ -345,8 +345,6 @@ Refer to `LICENSE` & `COPYRIGHT` files for formal wording.
---

## 16. Contact
Services: service@yazan-abo-ayash.de
Support: support@yazan-abo-ayash.de
Portfolio: https://www.coldbydefault.com
Documentation: https://docs.coldbydefault.com/
For professional or security inquiries, reach out via the official channels listed above.
Expand Down
41 changes: 29 additions & 12 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
# Security Policy

## Overview

This security policy outlines how we handle security vulnerabilities and the security measures implemented in this portfolio project.

## Supported Versions

Use this section to tell people about which versions of your project are
currently being supported with security updates.
Currently supported versions with security updates:

| Version | Supported |
| ------- | ------------------ |
| 5.0.x | :white_check_mark: |
| 4.0.x | :white_check_mark: |
| 3.0.x | :white_check_mark: |
| < 2.0 | :x: |
| Version | Supported | Notes |
| ------- | ------------------ | ------------------------------- |
| Latest | :white_check_mark: | Active development and security |
| < 3.0 | :x: | Legacy versions not supported |

## Reporting a Vulnerability

Use this section to tell people how to report a vulnerability.
If you discover a security vulnerability, please follow these steps:

### Where to Report

**Email**: See Contact Information.

## Security Audit History

| Date | Type | Status | Notes |
| ---------- | -------------- | ------ | ----------------------------- |
| 2026-02-16 | Internal Audit | ✅ | Security improvements applied |

## Additional Resources

- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [Next.js Security](https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy)
- [Prisma Security](https://www.prisma.io/docs/guides/database/advanced-database-tasks/sql-injection)

---

Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
**Copyright © 2026 ColdByDefault. All Rights Reserved.**
20 changes: 6 additions & 14 deletions app/(legals)/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
*/
*/

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
Expand Down Expand Up @@ -92,9 +92,7 @@ export default async function Privacy() {
<p className="text-sm font-medium mb-2">
{t("dataProcessing.vercelTitle")}
</p>
<p className="text-sm">
{t("dataProcessing.vercelDescription")}
</p>
<p className="text-sm">{t("dataProcessing.vercelDescription")}</p>
</div>
</CardContent>
</Card>
Expand All @@ -112,19 +110,15 @@ export default async function Privacy() {
</p>
<div className="p-4 rounded-lg border">
<p className="text-sm font-medium mb-2">
{t("chatbot.geminiTitle")}
</p>
<p className="text-sm">
{t("chatbot.geminiDescription")}
{t("chatbot.apiTitle")}
</p>
<p className="text-sm">{t("chatbot.apiDescription")}</p>
</div>
<div className="p-4 rounded-lg border">
<p className="text-sm font-medium mb-2">
{t("chatbot.temporaryTitle")}
</p>
<p className="text-sm">
{t("chatbot.temporaryDescription")}
</p>
<p className="text-sm">{t("chatbot.temporaryDescription")}</p>
</div>
</CardContent>
</Card>
Expand All @@ -144,9 +138,7 @@ export default async function Privacy() {
<p className="text-sm font-medium mb-2">
{t("booking.calendlyTitle")}
</p>
<p className="text-sm">
{t("booking.calendlyDescription")}
</p>
<p className="text-sm">{t("booking.calendlyDescription")}</p>
</div>
</CardContent>
</Card>
Expand Down
40 changes: 37 additions & 3 deletions app/api/about/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,39 @@
* @copyright 2026 ColdByDefault. All Rights Reserved.
*/

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { aboutData } from "@/data/main/aboutData";
import aboutProfile from "@/data/main/aboutProfile.json";
import { RateLimiter } from "@/lib/security";

// Rate limiter instance: 30 requests per minute
const rateLimiter = new RateLimiter(60000, 30);

function getClientIP(request: NextRequest): string {
const forwarded = request.headers.get("x-forwarded-for");
const realIp = request.headers.get("x-real-ip");
const cfConnectingIp = request.headers.get("cf-connecting-ip");
return cfConnectingIp || realIp || forwarded?.split(",")[0] || "127.0.0.1";
}

export function GET(request: NextRequest) {
// Rate limiting check
const clientIP = getClientIP(request);
if (!rateLimiter.isAllowed(clientIP)) {
return NextResponse.json(
{ error: "Too many requests" },
{
status: 429,
headers: {
"Retry-After": "60",
"X-RateLimit-Limit": "30",
"X-RateLimit-Remaining": "0",
},
},
);
}

export function GET() {
try {
const combinedData = {
...aboutData,
Expand All @@ -27,13 +55,19 @@ export function GET() {
status: 200,
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=86400",
"X-Content-Type-Options": "nosniff",
},
});
} catch (error) {
console.error("Error fetching about data:", error);
return NextResponse.json(
{ error: "Failed to fetch about data" },
{ status: 500 }
{
status: 500,
headers: {
"X-Content-Type-Options": "nosniff",
},
},
);
}
}
}
51 changes: 26 additions & 25 deletions app/api/admin/blog/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Blog Admin API Route
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
*/
*/

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
Expand Down Expand Up @@ -59,7 +59,7 @@ function getClientIP(request: NextRequest): string {

function isAuthorized(request: NextRequest): boolean {
if (!ADMIN_TOKEN) {
console.error("ADMIN_TOKEN environment variable not set");
// Security: Don't log sensitive information about environment configuration
return false;
}

Expand Down Expand Up @@ -188,7 +188,7 @@ const updateBlogSchema = z.object({
});

export async function GET(
request: NextRequest
request: NextRequest,
): Promise<NextResponse<BlogAdminResponse | ApiErrorResponse>> {
if (!isAuthorized(request)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Expand Down Expand Up @@ -218,22 +218,22 @@ export async function GET(
const page = parseInt(searchParams.get("page") || "1", 10);
const limit = Math.min(
parseInt(searchParams.get("limit") || "20", 10),
50
50,
);
const search = searchParams.get("search") || undefined;
const language = searchParams.get("language") || undefined;
const published =
searchParams.get("published") === "true"
? true
: searchParams.get("published") === "false"
? false
: undefined;
? false
: undefined;
const featured =
searchParams.get("featured") === "true"
? true
: searchParams.get("featured") === "false"
? false
: undefined;
? false
: undefined;

const queryParams: Partial<BlogListQuery> = {
page,
Expand All @@ -248,7 +248,7 @@ export async function GET(

const result = await getAdminBlogs(
context,
queryParams as BlogListQuery
queryParams as BlogListQuery,
);

return NextResponse.json({ success: true, data: result });
Expand All @@ -258,15 +258,15 @@ export async function GET(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
{ status: 400 }
{ status: 400 },
);
}

const blog = await getAdminBlogById(context, blogId);
if (!blog) {
return NextResponse.json(
{ error: "Blog not found" },
{ status: 404 }
{ status: 404 },
);
}

Expand Down Expand Up @@ -304,7 +304,7 @@ export async function GET(
}

export async function POST(
request: NextRequest
request: NextRequest,
): Promise<NextResponse<BlogAdminResponse | ApiErrorResponse>> {
if (!isAuthorized(request)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
Expand All @@ -324,7 +324,7 @@ export async function POST(
if (!contentType?.includes("application/json")) {
return NextResponse.json(
{ error: "Content-Type must be application/json" },
{ status: 400 }
{ status: 400 },
);
}

Expand All @@ -334,7 +334,7 @@ export async function POST(
} catch {
return NextResponse.json(
{ error: "Invalid JSON in request body" },
{ status: 400 }
{ status: 400 },
);
}

Expand All @@ -349,13 +349,13 @@ export async function POST(
error: "Validation failed",
details: parseResult.error.issues.map((issue) => issue.message),
},
{ status: 400 }
{ status: 400 },
);
}

const blog = await createBlog(
context,
parseResult.data as CreateBlogRequest
parseResult.data as CreateBlogRequest,
);

return NextResponse.json({
Expand All @@ -369,7 +369,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required for update" },
{ status: 400 }
{ status: 400 },
);
}

Expand All @@ -379,17 +379,18 @@ export async function POST(
{
error: "Validation failed",
details: parseResult.error.issues.map(
(issue: ZodIssue) => `${issue.path.join(".")}: ${issue.message}`
(issue: ZodIssue) =>
`${issue.path.join(".")}: ${issue.message}`,
),
},
{ status: 400 }
{ status: 400 },
);
}

const blog = await updateBlog(
context,
blogId,
parseResult.data as UpdateBlogRequest
parseResult.data as UpdateBlogRequest,
);

return NextResponse.json({
Expand All @@ -403,7 +404,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required for deletion" },
{ status: 400 }
{ status: 400 },
);
}

Expand All @@ -419,7 +420,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
{ status: 400 }
{ status: 400 },
);
}

Expand All @@ -438,7 +439,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
{ status: 400 }
{ status: 400 },
);
}

Expand All @@ -456,7 +457,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
{ status: 400 }
{ status: 400 },
);
}

Expand All @@ -473,7 +474,7 @@ export async function POST(
if (!blogId) {
return NextResponse.json(
{ error: "Blog ID is required" },
{ status: 400 }
{ status: 400 },
);
}

Expand Down
Loading
Loading