Initial commit with GitLab CI/CD pipeline

This commit is contained in:
Emmanuel
2026-01-06 22:17:37 +01:00
commit b5a5b973f7
12 changed files with 779 additions and 0 deletions

64
src/main.test.ts Normal file
View File

@@ -0,0 +1,64 @@
import { assertEquals, assertExists } from "https://deno.land/std@0.208.0/assert/mod.ts";
// Test configuration
const BASE_URL = "http://localhost:8000";
const VALID_TOKEN = Deno.env.get("BEARER_TOKEN") || "token";
Deno.test({
name: "Authentication - should reject invalid or missing tokens",
async fn() {
// Test missing token
const noTokenResponse = await fetch(`${BASE_URL}/languages`);
const noTokenData = await noTokenResponse.json();
assertEquals(noTokenResponse.status, 401);
assertEquals(noTokenData.error, "Missing or invalid Authorization header");
// Test invalid token
const invalidTokenResponse = await fetch(`${BASE_URL}/languages`, {
headers: { "Authorization": "Bearer wrong-token" },
});
const invalidTokenData = await invalidTokenResponse.json();
assertEquals(invalidTokenResponse.status, 403);
assertEquals(invalidTokenData.error, "Invalid bearer token");
// Test valid token works
const validTokenResponse = await fetch(`${BASE_URL}/languages`, {
headers: { "Authorization": `Bearer ${VALID_TOKEN}` },
});
await validTokenResponse.json(); // Consume the response body
assertEquals(validTokenResponse.status, 200);
},
});
Deno.test({
name: "Translation - should translate text with exclamation mark",
async fn() {
const response = await fetch(`${BASE_URL}/translate`, {
method: "POST",
headers: {
"Authorization": `Bearer ${VALID_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Hello world!",
target_lang: "FR",
}),
});
const data = await response.json();
assertEquals(response.status, 200);
assertExists(data.translations);
assertEquals(Array.isArray(data.translations), true);
assertEquals(data.translations.length, 1);
assertExists(data.translations[0].text);
assertExists(data.translations[0].detected_source_language);
// Verify the translation contains French text
const translatedText = data.translations[0].text;
assertEquals(typeof translatedText, "string");
assertEquals(translatedText.length > 0, true);
},
});

141
src/main.ts Normal file
View File

@@ -0,0 +1,141 @@
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
import { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
const DEEPL_API_KEY = Deno.env.get("DEEPL_AUTH_KEY");
const BEARER_TOKEN = Deno.env.get("BEARER_TOKEN");
const PORT = parseInt(Deno.env.get("PORT") || "8000");
const DEEPL_API_BASE = "https://api-free.deepl.com/v2";
const FRONTEND_URL = "https://deep-lite.netlify.app";
if (!DEEPL_API_KEY) {
console.error("ERROR: DEEPL_AUTH_KEY environment variable is required");
Deno.exit(1);
}
if (!BEARER_TOKEN) {
console.error("ERROR: BEARER_TOKEN environment variable is required");
Deno.exit(1);
}
const app = new Application();
const router = new Router();
// CORS middleware - only allow requests from the PWA frontend
app.use(
oakCors({
origin: FRONTEND_URL,
credentials: true,
}),
);
// Bearer token authentication middleware (skip /health endpoint)
app.use(async (ctx, next) => {
// Skip authentication for health check
if (ctx.request.url.pathname === "/health") {
await next();
return;
}
const authHeader = ctx.request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
ctx.response.status = 401;
ctx.response.body = { error: "Missing or invalid Authorization header" };
return;
}
const token = authHeader.substring(7); // Remove "Bearer " prefix
if (token !== BEARER_TOKEN) {
ctx.response.status = 403;
ctx.response.body = { error: "Invalid bearer token" };
return;
}
await next();
});
// POST /translate - Proxy to DeepL translate endpoint
router.post("/translate", async (ctx) => {
try {
const body = await ctx.request.body({ type: "json" }).value;
if (!body.text || !body.target_lang) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing required fields: text, target_lang" };
return;
}
// Build form data for DeepL API
const formData = new URLSearchParams();
formData.append("text", body.text);
formData.append("target_lang", body.target_lang);
if (body.source_lang) {
formData.append("source_lang", body.source_lang);
}
const response = await fetch(`${DEEPL_API_BASE}/translate`, {
method: "POST",
headers: {
"Authorization": `DeepL-Auth-Key ${DEEPL_API_KEY}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
});
const data = await response.json();
if (!response.ok) {
ctx.response.status = response.status;
ctx.response.body = data;
return;
}
ctx.response.body = data;
} catch (error) {
console.error("Translation error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// GET /languages - Proxy to DeepL languages endpoint
router.get("/languages", async (ctx) => {
try {
const response = await fetch(`${DEEPL_API_BASE}/languages`, {
method: "GET",
headers: {
"Authorization": `DeepL-Auth-Key ${DEEPL_API_KEY}`,
},
});
const data = await response.json();
if (!response.ok) {
ctx.response.status = response.status;
ctx.response.body = data;
return;
}
ctx.response.body = data;
} catch (error) {
console.error("Languages error:", error);
ctx.response.status = 500;
ctx.response.body = { error: "Internal server error" };
}
});
// Health check endpoint
router.get("/health", (ctx) => {
ctx.response.body = { status: "ok", timestamp: new Date().toISOString() };
});
app.use(router.routes());
app.use(router.allowedMethods());
console.log(`🚀 DeepLite BFF server running on http://localhost:${PORT}`);
console.log(`📝 CORS enabled for: ${FRONTEND_URL}`);
console.log(`🔒 Bearer token authentication enabled`);
await app.listen({ port: PORT });