Sesiones correctas
This commit is contained in:
@@ -13,9 +13,9 @@ services:
|
|||||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
- ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
#- ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5433:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-servicemanager}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-servicemanager}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -215,7 +215,7 @@ services:
|
|||||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
- uploads_data:/var/www/uploads:ro
|
- uploads_data:/var/www/uploads:ro
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "8088:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
- frontend-client
|
- frontend-client
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
const cookie = event.request.headers.get('cookie') ?? '';
|
// No restaurar sesión en la página de login
|
||||||
if (cookie) {
|
if (event.url.pathname === '/login') {
|
||||||
try {
|
event.locals.user = null;
|
||||||
const apiUrl = process.env.PUBLIC_API_URL ?? 'http://backend:8000';
|
return resolve(event);
|
||||||
const response = await fetch(`${apiUrl}/v1/auth/me`, {
|
}
|
||||||
headers: { cookie, 'X-App': 'client' }
|
|
||||||
});
|
const cookieHeader = event.request.headers.get('cookie') ?? '';
|
||||||
event.locals.user = response.ok ? await response.json() : null;
|
const cookieMatch = cookieHeader.match(/(?:client_access_token|internal_access_token)=([^;]+)/);
|
||||||
} catch {
|
const token = cookieMatch?.[1];
|
||||||
event.locals.user = null;
|
|
||||||
}
|
if (token) {
|
||||||
} else {
|
try {
|
||||||
event.locals.user = null;
|
const apiUrl = process.env.PUBLIC_API_URL ?? 'http://backend:8000';
|
||||||
}
|
const response = await fetch(`${apiUrl}/v1/auth/me`, {
|
||||||
return resolve(event);
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'X-App': 'client',
|
||||||
|
'X-Tenant-Slug': 'aduanasoft'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
event.locals.user = await response.json();
|
||||||
|
} else {
|
||||||
|
event.locals.user = null;
|
||||||
|
event.cookies.delete('client_access_token', { path: '/' });
|
||||||
|
event.cookies.delete('internal_access_token', { path: '/' });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
event.locals.user = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event.locals.user = null;
|
||||||
|
}
|
||||||
|
return resolve(event);
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
// Types
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -13,131 +11,94 @@ export interface User {
|
|||||||
is_two_factor_enabled: boolean;
|
is_two_factor_enabled: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
tenant_slug: string;
|
tenant_slug: string;
|
||||||
totp_code?: string;
|
totp_code?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type: string;
|
token_type: string;
|
||||||
expires_in: number;
|
expires_in: number;
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial state
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
user: null,
|
user: null,
|
||||||
token: null,
|
token: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create auth store
|
|
||||||
function createAuthStore() {
|
function createAuthStore() {
|
||||||
const { subscribe, set, update }: Writable<AuthState> = writable(initialState);
|
const { subscribe, set, update }: Writable<AuthState> = writable(initialState);
|
||||||
|
let _state = initialState;
|
||||||
|
subscribe(s => { _state = s; });
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
|
|
||||||
// Rehidrata sesión desde cookie HttpOnly (no toca localStorage)
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/auth/me', {
|
const response = await fetch('/api/v1/auth/me', {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: { 'X-App': 'client' }
|
headers: { 'X-App': 'client', 'X-Tenant-Slug': 'aduanasoft' }
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const user = await response.json();
|
const user = await response.json();
|
||||||
set({
|
set({ user, token: null, isAuthenticated: true, isLoading: false });
|
||||||
user,
|
|
||||||
token: null,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isLoading: false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// 401/400 es esperado cuando no hay sesión activa — no es un error
|
} catch (error) {}
|
||||||
} catch (error) {
|
|
||||||
// Ignorar errores de red en init
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Login
|
|
||||||
login: async (credentials: LoginRequest): Promise<void> => {
|
login: async (credentials: LoginRequest): Promise<void> => {
|
||||||
update(state => ({ ...state, isLoading: true }));
|
update(state => ({ ...state, isLoading: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/auth/login', {
|
const response = await fetch('/api/v1/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-Slug': credentials.tenant_slug,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(credentials)
|
body: JSON.stringify(credentials)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.detail || 'Login failed');
|
throw new Error(error.detail || 'Login failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: LoginResponse = await response.json();
|
const data: LoginResponse = await response.json();
|
||||||
|
set({ user: data.user, token: data.access_token, isAuthenticated: true, isLoading: false });
|
||||||
set({
|
|
||||||
user: data.user,
|
|
||||||
token: data.access_token,
|
|
||||||
isAuthenticated: true,
|
|
||||||
isLoading: false
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
update(state => ({ ...state, isLoading: false }));
|
update(state => ({ ...state, isLoading: false }));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Logout
|
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
// Llamar al backend para que borre la cookie HttpOnly
|
|
||||||
try {
|
try {
|
||||||
|
const token = _state.token;
|
||||||
await fetch('/api/v1/auth/logout', {
|
await fetch('/api/v1/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: { 'X-App': 'client' }
|
headers: {
|
||||||
|
'X-App': 'client',
|
||||||
|
'X-Tenant-Slug': 'aduanasoft',
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch { /* ignorar errores de red */ }
|
} catch {}
|
||||||
set(initialState);
|
set(initialState);
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateUser: (user: User) => { update(state => ({ ...state, user })); },
|
||||||
// Update user data
|
setUser: (user: User) => { set({ user, token: null, isAuthenticated: true, isLoading: false }); },
|
||||||
updateUser: (user: User) => {
|
setLoading: (isLoading: boolean) => { update(state => ({ ...state, isLoading })); }
|
||||||
update(state => ({ ...state, user }));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Set user from SSR pre-load (no fetch required)
|
|
||||||
setUser: (user: User) => {
|
|
||||||
set({ user, token: null, isAuthenticated: true, isLoading: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
// Set loading state
|
|
||||||
setLoading: (isLoading: boolean) => {
|
|
||||||
update(state => ({ ...state, isLoading }));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const auth = createAuthStore();
|
export const auth = createAuthStore();
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
/**
|
|
||||||
* Cliente HTTP centralizado para frontend-client.
|
|
||||||
* Usa cookies HttpOnly (client_access_token) como fuente primaria de auth,
|
|
||||||
* con Bearer token como complemento cuando está disponible en memoria.
|
|
||||||
*/
|
|
||||||
import { auth } from '$lib/stores/auth';
|
import { auth } from '$lib/stores/auth';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
const API_BASE = '/api/v1';
|
const API_BASE = '/api/v1';
|
||||||
|
const TENANT_SLUG = 'aduanasoft';
|
||||||
|
|
||||||
interface RequestOptions extends RequestInit {
|
interface RequestOptions extends RequestInit {
|
||||||
params?: Record<string, string>;
|
params?: Record<string, string>;
|
||||||
@@ -20,7 +16,6 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
|||||||
const filteredParams = Object.entries(params)
|
const filteredParams = Object.entries(params)
|
||||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||||
|
|
||||||
if (Object.keys(filteredParams).length > 0) {
|
if (Object.keys(filteredParams).length > 0) {
|
||||||
url += `?${new URLSearchParams(filteredParams).toString()}`;
|
url += `?${new URLSearchParams(filteredParams).toString()}`;
|
||||||
}
|
}
|
||||||
@@ -29,7 +24,6 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
|||||||
const authState = get(auth);
|
const authState = get(auth);
|
||||||
const headers = new Headers(init.headers);
|
const headers = new Headers(init.headers);
|
||||||
|
|
||||||
// Bearer header cuando el token está en memoria (sesión activa sin reload)
|
|
||||||
if (authState.token) {
|
if (authState.token) {
|
||||||
headers.set('Authorization', `Bearer ${authState.token}`);
|
headers.set('Authorization', `Bearer ${authState.token}`);
|
||||||
}
|
}
|
||||||
@@ -39,8 +33,8 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
|
|||||||
if (!headers.has('Content-Type')) {
|
if (!headers.has('Content-Type')) {
|
||||||
headers.set('Content-Type', 'application/json');
|
headers.set('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
// Identifica este frontend para que el backend use client_access_token
|
|
||||||
headers.set('X-App', 'client');
|
headers.set('X-App', 'client');
|
||||||
|
headers.set('X-Tenant-Slug', TENANT_SLUG);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...init,
|
...init,
|
||||||
@@ -78,6 +72,7 @@ async function downloadFile(endpoint: string, filename: string): Promise<void> {
|
|||||||
headers.set('X-Tenant-ID', authState.user.tenant_id);
|
headers.set('X-Tenant-ID', authState.user.tenant_id);
|
||||||
}
|
}
|
||||||
headers.set('X-App', 'client');
|
headers.set('X-App', 'client');
|
||||||
|
headers.set('X-Tenant-Slug', TENANT_SLUG);
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -108,19 +103,14 @@ async function downloadFile(endpoint: string, filename: string): Promise<void> {
|
|||||||
export const api = {
|
export const api = {
|
||||||
get: <T>(endpoint: string, params?: Record<string, string>) =>
|
get: <T>(endpoint: string, params?: Record<string, string>) =>
|
||||||
request<T>(endpoint, { method: 'GET', params }),
|
request<T>(endpoint, { method: 'GET', params }),
|
||||||
|
|
||||||
post: <T>(endpoint: string, body?: any) =>
|
post: <T>(endpoint: string, body?: any) =>
|
||||||
request<T>(endpoint, { method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
request<T>(endpoint, { method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
||||||
|
|
||||||
put: <T>(endpoint: string, body?: any) =>
|
put: <T>(endpoint: string, body?: any) =>
|
||||||
request<T>(endpoint, { method: 'PUT', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
request<T>(endpoint, { method: 'PUT', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
||||||
|
|
||||||
patch: <T>(endpoint: string, body?: any) =>
|
patch: <T>(endpoint: string, body?: any) =>
|
||||||
request<T>(endpoint, { method: 'PATCH', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
request<T>(endpoint, { method: 'PATCH', body: body !== undefined ? JSON.stringify(body) : undefined }),
|
||||||
|
|
||||||
delete: <T>(endpoint: string) =>
|
delete: <T>(endpoint: string) =>
|
||||||
request<T>(endpoint, { method: 'DELETE' }),
|
request<T>(endpoint, { method: 'DELETE' }),
|
||||||
|
|
||||||
downloadFile: (endpoint: string, filename: string) =>
|
downloadFile: (endpoint: string, filename: string) =>
|
||||||
downloadFile(endpoint, filename)
|
downloadFile(endpoint, filename)
|
||||||
};
|
};
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
let email = '';
|
let email = '';
|
||||||
let password = '';
|
let password = '';
|
||||||
let tenantSlug = 'aduanasoft-demo';
|
let tenantSlug = 'aduanasoft';
|
||||||
let totpCode = '';
|
let totpCode = '';
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let showTwoFactor = false;
|
let showTwoFactor = false;
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
await auth.login({
|
await auth.login({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
tenant_slug: tenantSlug.trim() || 'aduanasoft-demo',
|
tenant_slug: tenantSlug.trim() || 'aduanasoft',
|
||||||
totp_code: totpCode || undefined
|
totp_code: totpCode || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,35 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
watch: {
|
watch: {
|
||||||
usePolling: true,
|
usePolling: true,
|
||||||
interval: 500
|
interval: 500
|
||||||
},
|
},
|
||||||
// HMR: el browser llega al contenedor en el mismo puerto 3000
|
hmr: {
|
||||||
hmr: {
|
host: 'localhost',
|
||||||
host: 'localhost',
|
clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3000')
|
||||||
clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3000')
|
},
|
||||||
},
|
fs: {
|
||||||
// Permitir que Vite sirva archivos del filesystem del contenedor
|
allow: ['/app', '.'],
|
||||||
fs: {
|
strict: false
|
||||||
allow: ['/app', '.'],
|
},
|
||||||
strict: false
|
proxy: {
|
||||||
},
|
'/api': {
|
||||||
proxy: {
|
target: process.env.PUBLIC_API_URL || 'http://backend:8000',
|
||||||
'/api': {
|
changeOrigin: true,
|
||||||
target: process.env.PUBLIC_API_URL || 'http://localhost:8000',
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
changeOrigin: true,
|
}
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
}
|
||||||
}
|
},
|
||||||
}
|
preview: {
|
||||||
},
|
port: 3000,
|
||||||
preview: {
|
host: '0.0.0.0'
|
||||||
port: 3000,
|
},
|
||||||
host: '0.0.0.0'
|
build: {
|
||||||
},
|
target: 'esnext'
|
||||||
build: {
|
}
|
||||||
target: 'esnext'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
@@ -102,6 +102,7 @@ function createAuthStore() {
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-Slug': credentials.tenant_slug,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(credentials)
|
body: JSON.stringify(credentials)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
await auth.login({
|
await auth.login({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
tenant_slug: 'aduanasoft-demo',
|
tenant_slug: 'aduanasoft',
|
||||||
totp_code: totpCode || undefined
|
totp_code: totpCode || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,35 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
server: {
|
server: {
|
||||||
// Puerto: dentro del contenedor siempre 3000; Docker mapea 3001:3000 al host
|
port: parseInt(process.env.PORT || '3001'),
|
||||||
port: parseInt(process.env.PORT || '3001'),
|
host: '0.0.0.0',
|
||||||
host: '0.0.0.0',
|
watch: {
|
||||||
watch: {
|
usePolling: true,
|
||||||
usePolling: true,
|
interval: 500
|
||||||
interval: 500
|
},
|
||||||
},
|
hmr: {
|
||||||
// HMR: el browser llega al contenedor a través del puerto 3001 del host
|
host: 'localhost',
|
||||||
hmr: {
|
clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3001')
|
||||||
host: 'localhost',
|
},
|
||||||
clientPort: parseInt(process.env.HMR_CLIENT_PORT || '3001')
|
fs: {
|
||||||
},
|
allow: ['/app', '.'],
|
||||||
// Permitir que Vite sirva archivos del filesystem del contenedor
|
strict: false
|
||||||
fs: {
|
},
|
||||||
allow: ['/app', '.'],
|
proxy: {
|
||||||
strict: false
|
'/api': {
|
||||||
},
|
target: process.env.PUBLIC_API_URL || 'http://backend:8000',
|
||||||
proxy: {
|
changeOrigin: true,
|
||||||
'/api': {
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
target: process.env.PUBLIC_API_URL || 'http://localhost:8000',
|
}
|
||||||
changeOrigin: true,
|
}
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
},
|
||||||
}
|
preview: {
|
||||||
}
|
port: parseInt(process.env.PORT || '3001'),
|
||||||
},
|
host: '0.0.0.0'
|
||||||
preview: {
|
},
|
||||||
port: parseInt(process.env.PORT || '3001'),
|
build: {
|
||||||
host: '0.0.0.0'
|
target: 'esnext'
|
||||||
},
|
}
|
||||||
build: {
|
|
||||||
target: 'esnext'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
migrations.sql
Normal file
BIN
migrations.sql
Normal file
Binary file not shown.
Reference in New Issue
Block a user