Initial commit with GitLab CI/CD pipeline
This commit is contained in:
64
src/main.test.ts
Normal file
64
src/main.test.ts
Normal 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
141
src/main.ts
Normal 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 });
|
||||
Reference in New Issue
Block a user