This commit is contained in:
+98
-1
@@ -10,6 +10,7 @@ export default {
|
||||
if (url.pathname === '/auth/callback') return finishLogin(request, env);
|
||||
if (url.pathname === '/auth/logout') return logout(request, env);
|
||||
if (url.pathname === '/auth/session') return sessionResponse(request, env);
|
||||
if (url.pathname === '/api/feedbacks') return createFeedback(request, env);
|
||||
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
return json({ error: 'method_not_allowed' }, 405);
|
||||
@@ -141,6 +142,21 @@ function randomString(size = 32) {
|
||||
return base64url(randomBytes(size));
|
||||
}
|
||||
|
||||
function uuidv7() {
|
||||
const bytes = randomBytes(16);
|
||||
const timestamp = Date.now();
|
||||
bytes[0] = Math.floor(timestamp / 0x10000000000) & 0xff;
|
||||
bytes[1] = Math.floor(timestamp / 0x100000000) & 0xff;
|
||||
bytes[2] = Math.floor(timestamp / 0x1000000) & 0xff;
|
||||
bytes[3] = Math.floor(timestamp / 0x10000) & 0xff;
|
||||
bytes[4] = Math.floor(timestamp / 0x100) & 0xff;
|
||||
bytes[5] = timestamp & 0xff;
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
function getCookie(request, name) {
|
||||
const header = request.headers.get('cookie') || '';
|
||||
const match = header.split(';').map((part) => part.trim()).find((part) => part.startsWith(name + '='));
|
||||
@@ -166,6 +182,8 @@ function requiredAuthConfig(env) {
|
||||
function normalizeClaims(claims) {
|
||||
const profile = claims.profile && typeof claims.profile === 'object' ? claims.profile : {};
|
||||
const custom = claims.customAttributes || claims.custom_attributes || {};
|
||||
const profilePhones = Array.isArray(profile.phones) ? profile.phones : [];
|
||||
const profilePhone = profile.phone || profile.phone_number || profilePhones[0]?.number || profilePhones[0]?.value || profilePhones[0] || '';
|
||||
const tenants = claims.tenants || claims.tenantIds || custom.tenants || [];
|
||||
const tenantList = Array.isArray(tenants) ? tenants : Object.keys(tenants).map((key) => ({ id: key, ...(tenants[key] || {}) }));
|
||||
const tenantId = claims.tenant_id || claims.tenantId || custom.tenant_id || custom.tenantId || tenantList[0]?.id || tenantList[0]?.tenantId || '';
|
||||
@@ -184,7 +202,7 @@ function normalizeClaims(claims) {
|
||||
loginId: claims.email || claims.loginId || claims.preferred_username || profile.email || '',
|
||||
email: claims.email || profile.email || '',
|
||||
name: claims.name || profile.name || claims.display_name || '',
|
||||
phone: claims.phone || claims.phone_number || '',
|
||||
phone: claims.phone || claims.phone_number || profilePhone,
|
||||
company: custom.company || claims.company || claims.organization || '',
|
||||
familyCompany: custom.familyCompany || custom.family_company || claims.familyCompany || '',
|
||||
department: custom.team || custom.department || claims.department || ''
|
||||
@@ -267,6 +285,85 @@ async function sessionResponse(request, env) {
|
||||
return json({ authenticated: Boolean(user), user }, 200, { 'access-control-allow-credentials': 'true' });
|
||||
}
|
||||
|
||||
async function createFeedback(request, env) {
|
||||
if (request.method !== 'POST') return json({ error: 'method_not_allowed' }, 405, { allow: 'POST' });
|
||||
const user = await getSession(request, env);
|
||||
if (!user) return json({ error: 'unauthenticated' }, 401);
|
||||
if (!env.ABC_API_KEY) return json({ error: 'feedback_api_not_configured' }, 503);
|
||||
if (!env.ABC_PROJECT_ID || !env.ABC_CHANNEL_ID) return json({ error: 'feedback_target_not_configured' }, 503);
|
||||
|
||||
let input;
|
||||
try {
|
||||
input = await request.json();
|
||||
} catch (error) {
|
||||
return json({ error: 'invalid_json' }, 400);
|
||||
}
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) return json({ error: 'invalid_json' }, 400);
|
||||
|
||||
const title = String(input.title || '').trim();
|
||||
const contents = String(input.contents || '').trim();
|
||||
const category = String(input.Category || '').trim();
|
||||
if (!title || !contents || !category) return json({ error: 'title_contents_category_required' }, 400);
|
||||
if (!['ERROR_QNA', 'IMPROVEMENT_QNA', 'GENERAL_QNA'].includes(category)) return json({ error: 'invalid_category' }, 400);
|
||||
|
||||
const feedbackId = isUuidv7(input.feedbackId) ? input.feedbackId : uuidv7();
|
||||
const requesterId = user.requesterId || user.ssoSubject || user.userUuid;
|
||||
const requesterTenantId = user.requesterTenantId || user.tenantId;
|
||||
if (!requesterId || !requesterTenantId) return json({ error: 'requester_identity_missing' }, 422);
|
||||
if (!user.phone) return json({ error: 'requester_phone_missing' }, 422);
|
||||
|
||||
const upstreamPayload = {
|
||||
title,
|
||||
contents,
|
||||
Category: category,
|
||||
requester_id: requesterId,
|
||||
requester_tenant_id: requesterTenantId,
|
||||
requester_email: user.email || '',
|
||||
requester_name: user.name || '',
|
||||
requester_department: user.department || '',
|
||||
requester_phone_number: user.phone,
|
||||
is_secret: input.is_secret ? 1 : 0
|
||||
};
|
||||
if (Array.isArray(input.images) && input.images.length) upstreamPayload.images = input.images;
|
||||
|
||||
const apiBaseUrl = String(env.ABC_API_BASE_URL || 'https://feedback.hmac.kr').replace(/\/$/, '');
|
||||
const endpoint = `${apiBaseUrl}/api/projects/${encodeURIComponent(env.ABC_PROJECT_ID)}/channels/${encodeURIComponent(env.ABC_CHANNEL_ID)}/feedbacks`;
|
||||
const sourceNamespace = env.ABC_SOURCE_NAMESPACE || 'EGBIM_QA';
|
||||
const idempotencyConsumer = env.ABC_IDEMPOTENCY_CONSUMER || sourceNamespace;
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'x-api-key': env.ABC_API_KEY,
|
||||
'x-source-namespace': sourceNamespace,
|
||||
'x-source-record-id': feedbackId,
|
||||
'x-idempotency-consumer': idempotencyConsumer,
|
||||
'idempotency-key': feedbackId
|
||||
},
|
||||
body: JSON.stringify(upstreamPayload)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('feedback_api_request_failed', error instanceof Error ? error.message : 'unknown');
|
||||
return json({ error: 'feedback_api_unreachable' }, 502);
|
||||
}
|
||||
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
console.error('feedback_api_rejected', response.status);
|
||||
return json({ error: 'feedback_api_rejected', status: response.status }, response.status >= 500 ? 502 : response.status);
|
||||
}
|
||||
const id = result.id || result.data?.id || result.feedback?.id;
|
||||
if (!id) return json({ error: 'feedback_id_missing' }, 502);
|
||||
return json({ id });
|
||||
}
|
||||
|
||||
function isUuidv7(value) {
|
||||
return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
||||
}
|
||||
|
||||
function logout(request, env) {
|
||||
const url = new URL(request.url);
|
||||
const target = safeReturnUrl(env.AUTH_POST_LOGOUT_REDIRECT_URI || '/', url.origin);
|
||||
|
||||
Reference in New Issue
Block a user