setup local dev env
This commit is contained in:
22
web/Dockerfile
Normal file
22
web/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM node:current-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
||||
COPY web ./web
|
||||
COPY rulesets ./rulesets
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm --filter @campaign-manager/web build
|
||||
|
||||
WORKDIR /app/web
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=5173
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
CMD ["node", "build"]
|
||||
@@ -1,38 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link href="/_app/immutable/entry/start.Pj34kLt-.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/YzYuob9f.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/CUCwB180.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/DNqN6DmX.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/entry/app.H3SWXino.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/Db9w--lA.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/pTMRHjpX.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/Bzak7iHL.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/C5YqYP7P.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_1smak34 = {
|
||||
base: ""
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("/_app/immutable/entry/start.Pj34kLt-.js"),
|
||||
import("/_app/immutable/entry/app.H3SWXino.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
export const env={}
|
||||
@@ -1 +0,0 @@
|
||||
var e;typeof window<"u"&&((e=window.__svelte??(window.__svelte={})).v??(e.v=new Set)).add("5");
|
||||
@@ -1 +0,0 @@
|
||||
import{s as c,g as l}from"./DNqN6DmX.js";import{b as o,d as b,n as a,m as d,g as p,e as g}from"./CUCwB180.js";let s=!1,i=Symbol();function m(e,u,r){const n=r[u]??(r[u]={store:null,source:d(void 0),unsubscribe:a});if(n.store!==e&&!(i in r))if(n.unsubscribe(),n.store=e??null,e==null)n.source.v=void 0,n.unsubscribe=a;else{var t=!0;n.unsubscribe=c(e,f=>{t?n.source.v=f:g(n.source,f)}),t=!1}return e&&i in r?l(e):p(n.source)}function y(){const e={};function u(){o(()=>{for(var r in e)e[r].unsubscribe();b(e,i,{enumerable:!1,value:!0})})}return[e,u]}function N(e){var u=s;try{return s=!1,[e(),s]}finally{s=u}}export{m as a,N as c,y as s};
|
||||
@@ -1 +0,0 @@
|
||||
import{w as a}from"./CUCwB180.js";a();
|
||||
@@ -1 +0,0 @@
|
||||
import{s as e}from"./YzYuob9f.js";const r=()=>{const s=e;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},b={subscribe(s){return r().page.subscribe(s)}};export{b as p};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{n as c,j as a,J as p,i as d,h as l,K as m}from"./CUCwB180.js";function h(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function g(e,n,s){if(e==null)return n(void 0),c;const u=a(()=>e.subscribe(n,s));return u.unsubscribe?()=>u.unsubscribe():u}const i=[];function q(e,n=c){let s=null;const u=new Set;function r(o){if(p(e,o)&&(e=o,s)){const f=!i.length;for(const t of u)t[1](),i.push(t,e);if(f){for(let t=0;t<i.length;t+=2)i[t][0](i[t+1]);i.length=0}}}function b(o){r(o(e))}function _(o,f=c){const t=[o,f];return u.add(t),u.size===1&&(s=n(r,b)||c),o(e),()=>{u.delete(t),u.size===0&&s&&(s(),s=null)}}return{set:r,update:b,subscribe:_}}function k(e){let n;return g(e,s=>n=s)(),n}function x(e){l===null&&h(),m&&l.l!==null?w(l).m.push(e):d(()=>{const n=a(e);if(typeof n=="function")return n})}function w(e){var n=e.l;return n.u??(n.u={a:[],b:[],m:[]})}export{k as g,x as o,g as s,q as w};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{h as d,u as g,i as c,j as m,k as l,l as b,g as p,o as h,q as k}from"./CUCwB180.js";function x(n=!1){const s=d,e=s.l.u;if(!e)return;let r=()=>h(s.s);if(n){let o=0,t={};const _=k(()=>{let i=!1;const a=s.s;for(const f in a)a[f]!==t[f]&&(t[f]=a[f],i=!0);return i&&o++,o});r=()=>p(_)}e.b.length&&g(()=>{u(s,r),l(e.b)}),c(()=>{const o=m(()=>e.m.map(b));return()=>{for(const t of o)typeof t=="function"&&t()}}),e.a.length&&c(()=>{u(s,r),l(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}export{x as i};
|
||||
@@ -1 +0,0 @@
|
||||
import{x as h,y as d,z as _,A as l,B as p,T as E,C as g,D as u,E as s,R as y,F as x,G as A,H as M,I as N}from"./CUCwB180.js";var f;const i=((f=globalThis==null?void 0:globalThis.window)==null?void 0:f.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function b(t){return(i==null?void 0:i.createHTML(t))??t}function L(t){var r=h("template");return r.innerHTML=b(t.replaceAll("<!>","<!---->")),r.content}function n(t,r){var e=_;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function w(t,r){var e=(r&E)!==0,c=(r&g)!==0,a,m=!t.startsWith("<!>");return()=>{if(u)return n(s,null),s;a===void 0&&(a=L(m?t:"<!>"+t),e||(a=l(a)));var o=c||p?document.importNode(a,!0):a.cloneNode(!0);if(e){var T=l(o),v=o.lastChild;n(T,v)}else n(o,o);return o}}function C(t=""){if(!u){var r=d(t+"");return n(r,r),r}var e=s;return e.nodeType!==A?(e.before(e=d()),M(e)):N(e),n(e,e),e}function D(){if(u)return n(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=d();return t.append(r,e),n(r,e),t}function H(t,r){if(u){var e=_;((e.f&y)===0||e.nodes.end===null)&&(e.nodes.end=s),x();return}t!==null&&t.before(r)}export{H as a,n as b,D as c,w as f,C as t};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{l as o,a as r}from"../chunks/YzYuob9f.js";export{o as load_css,r as start};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{o as h}from"../chunks/DNqN6DmX.js";import{D as m,F as g,p as y,f as T,a as $}from"../chunks/CUCwB180.js";import{s as _,a as k}from"../chunks/C5YqYP7P.js";import{c as v,a as w}from"../chunks/pTMRHjpX.js";import{i as A}from"../chunks/eiK12uJk.js";import{g as u}from"../chunks/YzYuob9f.js";import{p as O}from"../chunks/CGowzvwH.js";function S(t,e,a,n,o){var f;m&&g();var s=(f=e.$$slots)==null?void 0:f[a],r=!1;s===!0&&(s=e.children,r=!0),s===void 0||s(t,r?()=>n:n)}const d="https://api.campaign-manager.example.com/v1";let i=null;function E(t){i=t}async function P(){const t=await fetch(`${d}/auth/refresh`,{method:"POST",credentials:"include"});return t.ok?(i=(await t.json()).accessToken,!0):(i=null,!1)}async function c(t,e={}){const{skipAuth:a=!1,...n}=e,o=new Headers(n.headers);!a&&i&&o.set("Authorization",`Bearer ${i}`),n.body&&!o.has("Content-Type")&&o.set("Content-Type","application/json");let s=await fetch(`${d}${t}`,{...n,headers:o,credentials:"include"});return s.status===401&&!a&&await P()&&i&&(o.set("Authorization",`Bearer ${i}`),s=await fetch(`${d}${t}`,{...n,headers:o,credentials:"include"})),s}const B={get:(t,e)=>c(t,{...e,method:"GET"}),post:(t,e,a)=>c(t,{...a,method:"POST",body:e?JSON.stringify(e):void 0}),put:(t,e,a)=>c(t,{...a,method:"PUT",body:e?JSON.stringify(e):void 0}),patch:(t,e,a)=>c(t,{...a,method:"PATCH",body:e?JSON.stringify(e):void 0}),delete:(t,e)=>c(t,{...e,method:"DELETE"})};function z(t,e){y(e,!1);const a=()=>k(O,"$page",n),[n,o]=_(),s=["/login"];h(async()=>{if(!s.includes(a().url.pathname))try{const l=await B.post("/auth/refresh",void 0,{skipAuth:!0});if(l.ok){const p=await l.json();E(p.accessToken)}else u("/login")}catch{u("/login")}}),A();var r=v(),f=T(r);S(f,e,"default",{}),w(t,r),$(),o()}export{z as component};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{p as h,f as g,t as l,a as v,c as e,r as o,s as d}from"../chunks/CUCwB180.js";import{s as p}from"../chunks/Db9w--lA.js";import{a as _,f as x}from"../chunks/pTMRHjpX.js";import{i as $}from"../chunks/eiK12uJk.js";import{s as k,p as m}from"../chunks/YzYuob9f.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var E=x("<h1> </h1> <p> </p>",1);function C(f,c){h(c,!1),$();var t=E(),r=g(t),n=e(r,!0);o(r);var s=d(r,2),u=e(s,!0);o(s),l(()=>{var a;p(n,i.status),p(u,(a=i.error)==null?void 0:a.message)}),_(f,t),v()}export{C as component};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{o as p}from"../chunks/DNqN6DmX.js";import{p as r,a as t}from"../chunks/CUCwB180.js";import{i as m}from"../chunks/eiK12uJk.js";import{g as a}from"../chunks/YzYuob9f.js";function c(i,o){r(o,!1),p(()=>a("/groups")),m(),t()}export{c as component};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{a,f as t}from"../chunks/pTMRHjpX.js";var p=t("<h1>Your Characters</h1>");function n(o){var r=p();a(o,r)}export{n as component};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{p as i,s as f,f as c,t as n,a as h,c as g,r as l}from"../chunks/CUCwB180.js";import{s as _,a as $}from"../chunks/C5YqYP7P.js";import{s as d}from"../chunks/Db9w--lA.js";import{a as u,f as v}from"../chunks/pTMRHjpX.js";import{i as x}from"../chunks/eiK12uJk.js";import{p as C}from"../chunks/CGowzvwH.js";var b=v("<h1>Character Sheet</h1> <p> </p>",1);function z(r,s){i(s,!1);const e=()=>$(C,"$page",p),[p,o]=_();x();var a=b(),t=f(c(a),2),m=g(t);l(t),n(()=>d(m,`Character ID: ${e().params.id??""}`)),u(r,a),h(),o()}export{z as component};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{a as p,f as a}from"../chunks/pTMRHjpX.js";var t=a("<h1>Your Groups</h1>");function f(o){var r=t();p(o,r)}export{f as component};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{p as i,s as f,f as n,t as c,a as l,c as g,r as _}from"../chunks/CUCwB180.js";import{s as $,a as h}from"../chunks/C5YqYP7P.js";import{s as u}from"../chunks/Db9w--lA.js";import{a as d,f as v}from"../chunks/pTMRHjpX.js";import{i as x}from"../chunks/eiK12uJk.js";import{p as D}from"../chunks/CGowzvwH.js";var G=v("<h1>Group Detail</h1> <p> </p>",1);function A(s,r){i(r,!1);const p=()=>h(D,"$page",o),[o,e]=$();x();var a=G(),t=f(n(a),2),m=g(t);_(t),c(()=>u(m,`Group ID: ${p().params.id??""}`)),d(s,a),l(),e()}export{A as component};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{p as i,s as f,f as n,t as c,a as g,c as l,r as _}from"../chunks/CUCwB180.js";import{s as $,a as h}from"../chunks/C5YqYP7P.js";import{s as u}from"../chunks/Db9w--lA.js";import{a as d,f as v}from"../chunks/pTMRHjpX.js";import{i as x}from"../chunks/eiK12uJk.js";import{p as G}from"../chunks/CGowzvwH.js";var b=v("<h1>Group Settings</h1> <p> </p>",1);function z(a,r){i(r,!1);const p=()=>h(G,"$page",o),[o,e]=$();x();var t=b(),s=f(n(t),2),m=l(s);_(s),c(()=>u(m,`Group ID: ${p().params.id??""}`)),d(a,t),g(),e()}export{z as component};
|
||||
@@ -1 +0,0 @@
|
||||
import"../chunks/Bzak7iHL.js";import"../chunks/CAYgxOZ1.js";import{v as r}from"../chunks/CUCwB180.js";import{a as m,f as n}from"../chunks/pTMRHjpX.js";var p=n("<h1>Campaign Manager</h1> <p>Login — coming soon</p>",1);function g(o){var a=p();r(2),m(o,a)}export{g as component};
|
||||
@@ -1 +0,0 @@
|
||||
{"version":"1773071061170"}
|
||||
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "node build",
|
||||
"start": "node build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
@@ -14,7 +15,7 @@
|
||||
"@campaign-manager/rulesets": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^6.0.0"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const BASE_URL = 'https://api.campaign-manager.example.com/v1';
|
||||
const CAMPAIGN_BASE_URL = '/api/campaign';
|
||||
const CONTENT_BASE_URL = '/api/content';
|
||||
const AUTH_BASE_URL = '/api/auth';
|
||||
|
||||
// Access token held in memory only — never persisted to localStorage
|
||||
let _accessToken: string | null = null;
|
||||
@@ -15,9 +17,14 @@ interface RequestOptions extends RequestInit {
|
||||
skipAuth?: boolean;
|
||||
}
|
||||
|
||||
function joinUrl(base: string, path: string): string {
|
||||
if (!path) return base;
|
||||
return `${base}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
async function silentRefresh(): Promise<boolean> {
|
||||
// Refresh token is in an HttpOnly cookie; no manual credential needed
|
||||
const res = await fetch(`${BASE_URL}/auth/refresh`, {
|
||||
const res = await fetch(joinUrl(AUTH_BASE_URL, '/refresh'), {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
@@ -32,7 +39,7 @@ async function silentRefresh(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function apiFetch(path: string, options: RequestOptions = {}): Promise<Response> {
|
||||
async function apiFetch(baseUrl: string, path: string, options: RequestOptions = {}): Promise<Response> {
|
||||
const { skipAuth = false, ...fetchOptions } = options;
|
||||
const headers = new Headers(fetchOptions.headers);
|
||||
|
||||
@@ -44,7 +51,9 @@ async function apiFetch(path: string, options: RequestOptions = {}): Promise<Res
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
let res = await fetch(`${BASE_URL}${path}`, {
|
||||
const targetUrl = joinUrl(baseUrl, path);
|
||||
|
||||
let res = await fetch(targetUrl, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
@@ -54,7 +63,7 @@ async function apiFetch(path: string, options: RequestOptions = {}): Promise<Res
|
||||
const refreshed = await silentRefresh();
|
||||
if (refreshed && _accessToken) {
|
||||
headers.set('Authorization', `Bearer ${_accessToken}`);
|
||||
res = await fetch(`${BASE_URL}${path}`, { ...fetchOptions, headers, credentials: 'include' });
|
||||
res = await fetch(targetUrl, { ...fetchOptions, headers, credentials: 'include' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,17 +72,39 @@ async function apiFetch(path: string, options: RequestOptions = {}): Promise<Res
|
||||
|
||||
export const api = {
|
||||
get: (path: string, options?: RequestOptions) =>
|
||||
apiFetch(path, { ...options, method: 'GET' }),
|
||||
apiFetch(CAMPAIGN_BASE_URL, path, { ...options, method: 'GET' }),
|
||||
|
||||
post: (path: string, body?: unknown, options?: RequestOptions) =>
|
||||
apiFetch(path, { ...options, method: 'POST', body: body ? JSON.stringify(body) : undefined }),
|
||||
apiFetch(CAMPAIGN_BASE_URL, path, { ...options, method: 'POST', body: body ? JSON.stringify(body) : undefined }),
|
||||
|
||||
put: (path: string, body?: unknown, options?: RequestOptions) =>
|
||||
apiFetch(path, { ...options, method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
|
||||
apiFetch(CAMPAIGN_BASE_URL, path, { ...options, method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
|
||||
|
||||
patch: (path: string, body?: unknown, options?: RequestOptions) =>
|
||||
apiFetch(path, { ...options, method: 'PATCH', body: body ? JSON.stringify(body) : undefined }),
|
||||
apiFetch(CAMPAIGN_BASE_URL, path, { ...options, method: 'PATCH', body: body ? JSON.stringify(body) : undefined }),
|
||||
|
||||
delete: (path: string, options?: RequestOptions) =>
|
||||
apiFetch(path, { ...options, method: 'DELETE' }),
|
||||
apiFetch(CAMPAIGN_BASE_URL, path, { ...options, method: 'DELETE' }),
|
||||
};
|
||||
|
||||
export const contentApi = {
|
||||
get: (path: string, options?: RequestOptions) =>
|
||||
apiFetch(CONTENT_BASE_URL, path, { ...options, method: 'GET' }),
|
||||
|
||||
post: (path: string, body?: unknown, options?: RequestOptions) =>
|
||||
apiFetch(CONTENT_BASE_URL, path, { ...options, method: 'POST', body: body ? JSON.stringify(body) : undefined }),
|
||||
|
||||
put: (path: string, body?: unknown, options?: RequestOptions) =>
|
||||
apiFetch(CONTENT_BASE_URL, path, { ...options, method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
|
||||
|
||||
patch: (path: string, body?: unknown, options?: RequestOptions) =>
|
||||
apiFetch(CONTENT_BASE_URL, path, { ...options, method: 'PATCH', body: body ? JSON.stringify(body) : undefined }),
|
||||
|
||||
delete: (path: string, options?: RequestOptions) =>
|
||||
apiFetch(CONTENT_BASE_URL, path, { ...options, method: 'DELETE' }),
|
||||
};
|
||||
|
||||
export const authApi = {
|
||||
post: (path: string, body?: unknown, options?: RequestOptions) =>
|
||||
apiFetch(AUTH_BASE_URL, path, { ...options, method: 'POST', body: body ? JSON.stringify(body) : undefined }),
|
||||
};
|
||||
|
||||
79
web/src/lib/server/proxy.ts
Normal file
79
web/src/lib/server/proxy.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
const FORWARDED_RESPONSE_HEADERS = new Set([
|
||||
'cache-control',
|
||||
'content-type',
|
||||
'etag',
|
||||
'last-modified',
|
||||
'set-cookie',
|
||||
'vary',
|
||||
'www-authenticate',
|
||||
]);
|
||||
|
||||
function resolveBaseUrl(raw: string | undefined, name: string): string {
|
||||
if (!raw) {
|
||||
throw error(500, `${name} is not configured`);
|
||||
}
|
||||
|
||||
return raw.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function buildTargetUrl(baseUrl: string, path: string | undefined, search: string): string {
|
||||
const normalizedPath = path ? `/${path}` : '';
|
||||
return `${baseUrl}${normalizedPath}${search}`;
|
||||
}
|
||||
|
||||
function copyRequestHeaders(headers: Headers): Headers {
|
||||
const forwarded = new Headers();
|
||||
|
||||
for (const [key, value] of headers.entries()) {
|
||||
const lowered = key.toLowerCase();
|
||||
if (lowered === 'host') continue;
|
||||
if (lowered === 'content-length') continue;
|
||||
forwarded.set(key, value);
|
||||
}
|
||||
|
||||
return forwarded;
|
||||
}
|
||||
|
||||
function copyResponseHeaders(headers: Headers): Headers {
|
||||
const forwarded = new Headers();
|
||||
|
||||
for (const [key, value] of headers.entries()) {
|
||||
if (FORWARDED_RESPONSE_HEADERS.has(key.toLowerCase())) {
|
||||
forwarded.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return forwarded;
|
||||
}
|
||||
|
||||
export async function proxyToService(
|
||||
event: RequestEvent,
|
||||
rawBaseUrl: string | undefined,
|
||||
baseName: string,
|
||||
): Promise<Response> {
|
||||
const baseUrl = resolveBaseUrl(rawBaseUrl, baseName);
|
||||
const url = new URL(event.request.url);
|
||||
const targetUrl = buildTargetUrl(baseUrl, event.params.path, url.search);
|
||||
|
||||
const init: RequestInit = {
|
||||
method: event.request.method,
|
||||
headers: copyRequestHeaders(event.request.headers),
|
||||
redirect: 'manual',
|
||||
};
|
||||
|
||||
if (!['GET', 'HEAD'].includes(event.request.method.toUpperCase())) {
|
||||
init.body = await event.request.arrayBuffer();
|
||||
}
|
||||
|
||||
const upstream = await fetch(targetUrl, init);
|
||||
const headers = copyResponseHeaders(upstream.headers);
|
||||
|
||||
return new Response(upstream.body, {
|
||||
status: upstream.status,
|
||||
statusText: upstream.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { api, setAccessToken } from '$lib/api';
|
||||
import { authApi, setAccessToken } from '$lib/api';
|
||||
|
||||
const PUBLIC_ROUTES = ['/login'];
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
// Attempt silent token refresh via HttpOnly cookie before rendering
|
||||
try {
|
||||
const res = await api.post('/auth/refresh', undefined, { skipAuth: true });
|
||||
const res = await authApi.post('/refresh', undefined, { skipAuth: true });
|
||||
if (res.ok) {
|
||||
const data = await res.json() as { accessToken: string };
|
||||
setAccessToken(data.accessToken);
|
||||
|
||||
19
web/src/routes/api/auth/[...path]/+server.ts
Normal file
19
web/src/routes/api/auth/[...path]/+server.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { proxyToService } from '$lib/server/proxy';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const CAMPAIGN_BASE = env.CAMPAIGN_API_BASE || 'http://localhost:3000/v1';
|
||||
|
||||
const handler: RequestHandler = (event) =>
|
||||
proxyToService(
|
||||
event,
|
||||
`${CAMPAIGN_BASE.replace(/\/+$/, '')}/auth`,
|
||||
'CAMPAIGN_API_BASE',
|
||||
);
|
||||
|
||||
export const GET = handler;
|
||||
export const POST = handler;
|
||||
export const PUT = handler;
|
||||
export const PATCH = handler;
|
||||
export const DELETE = handler;
|
||||
export const OPTIONS = handler;
|
||||
15
web/src/routes/api/campaign/[...path]/+server.ts
Normal file
15
web/src/routes/api/campaign/[...path]/+server.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { proxyToService } from '$lib/server/proxy';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const CAMPAIGN_BASE = env.CAMPAIGN_API_BASE || 'http://localhost:3000/v1';
|
||||
|
||||
const handler: RequestHandler = (event) =>
|
||||
proxyToService(event, CAMPAIGN_BASE, 'CAMPAIGN_API_BASE');
|
||||
|
||||
export const GET = handler;
|
||||
export const POST = handler;
|
||||
export const PUT = handler;
|
||||
export const PATCH = handler;
|
||||
export const DELETE = handler;
|
||||
export const OPTIONS = handler;
|
||||
15
web/src/routes/api/content/[...path]/+server.ts
Normal file
15
web/src/routes/api/content/[...path]/+server.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { proxyToService } from '$lib/server/proxy';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const CONTENT_BASE = env.CONTENT_API_BASE || 'http://localhost:3001/v1';
|
||||
|
||||
const handler: RequestHandler = (event) =>
|
||||
proxyToService(event, CONTENT_BASE, 'CONTENT_API_BASE');
|
||||
|
||||
export const GET = handler;
|
||||
export const POST = handler;
|
||||
export const PUT = handler;
|
||||
export const PATCH = handler;
|
||||
export const DELETE = handler;
|
||||
export const OPTIONS = handler;
|
||||
@@ -1,8 +1,8 @@
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
export default {
|
||||
kit: {
|
||||
adapter: adapter({ fallback: '404.html' }),
|
||||
adapter: adapter(),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user