This commit is contained in:
LeoMortari
2025-11-04 00:08:03 -03:00
9 changed files with 182 additions and 38 deletions

View File

@@ -1,7 +1,57 @@
# Dependencies
node_modules node_modules
# Build output
dist dist
# Git
.git .git
.gitignore .gitignore
# Docker files (não precisam estar dentro da imagem)
Dockerfile Dockerfile
docker-compose.yml docker-compose.yml
pnpm-lock.yaml .dockerignore
# Arquivos de lock (npm/yarn/pnpm)
package-lock.json
yarn.lock
# Mantém pnpm-lock.yaml pois é necessário para build reproduzível
# Documentação
*.md
README.md
# Environment files (usar apenas .env.example como referência)
.env
.env.local
.env.development
.env.production
.env.*.local
# IDE
.vscode
.idea
*.swp
*.swo
*~
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# OS
.DS_Store
Thumbs.db
# Testing
coverage
.nyc_output
# Cache
.cache
.eslintcache
.stylelintcache

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# Ambiente (development ou production)
VITE_ENV=production
# URL da API do back-end
# No Dokploy com Traefik, usar o nome do serviço interno
VITE_API_URL=http://clipperia-api:3000

View File

@@ -7,10 +7,11 @@ COPY . .
RUN pnpm install RUN pnpm install
ARG BASE=/ ARG BASE=/
ARG VITE_API_URL=https://api.clipperia.com.br
ENV VITE_BASE_PATH=$BASE ENV VITE_BASE_PATH=$BASE
RUN pnpm run build ENV VITE_API_URL=$VITE_API_URL
RUN pnpm build
RUN rm -rf node_modules RUN rm -rf node_modules
FROM nginx:1.27-alpine FROM nginx:1.27-alpine

View File

@@ -5,18 +5,21 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
BASE: ${BASE:-/} BASE: ${BASE:-/}
VITE_API_URL: ${VITE_API_URL:-https://api.clipperia.com.br}
no_cache: true no_cache: true
image: ${IMAGE_NAME:-vite-nginx:latest} image: ${IMAGE_NAME:-clipperia-web:latest}
container_name: clipperia container_name: clipperia
restart: unless-stopped restart: unless-stopped
networks: networks:
- dokploy-network - dokploy-network
# Labels do Traefik gerenciados pelo Dokploy
# Se necessário configurar manualmente:
# labels: # labels:
# - traefik.enable=true # - traefik.enable=true
# - traefik.http.routers.vite.rule=Host("clipperia.com.br") # - traefik.http.routers.clipperia.rule=Host(`clipperia.com.br`)
# - traefik.http.routers.vite.entrypoints=websecure # - traefik.http.routers.clipperia.entrypoints=websecure
# - traefik.http.routers.vite.tls.certresolver=letsencrypt # - traefik.http.routers.clipperia.tls.certresolver=letsencrypt
# - traefik.http.services.vite.loadbalancer.server.port=80 # - traefik.http.services.clipperia.loadbalancer.server.port=80
networks: networks:
dokploy-network: dokploy-network:
external: true external: true

View File

@@ -8,13 +8,35 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# Configurações para proxy reverso (Traefik)
real_ip_header X-Forwarded-For;
set_real_ip_from 0.0.0.0/0;
# Cabeçalhos de segurança
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Logs
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Cache de assets estáticos
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 7d; expires 7d;
add_header Cache-Control "public, max-age=604800, immutable"; add_header Cache-Control "public, max-age=604800, immutable";
try_files $uri =404; try_files $uri =404;
} }
# SPA routing - todas as rotas vão para index.html
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# Health check endpoint (opcional)
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
} }

View File

@@ -4,6 +4,7 @@ import { createWebHistory, createRouter } from "vue-router";
import roles from "@/auth/roles"; import roles from "@/auth/roles";
import { extractRolesFromToken } from "@/utils/keycloak"; import { extractRolesFromToken } from "@/utils/keycloak";
import { hasToken, validateToken, redirectToLogin } from "@/utils/auth";
import Videos from "@/routes/videos"; import Videos from "@/routes/videos";
import NewVideo from "@/routes/videos/new"; import NewVideo from "@/routes/videos/new";
@@ -139,37 +140,49 @@ const hasPermission = (requiredPermissions = []) => {
); );
}; };
// router.beforeEach((to, from, next) => { router.beforeEach(async (to, from, next) => {
// if (to.meta.title) { if (to.meta.title) {
// document.title = to.meta.title; document.title = to.meta.title;
// } }
// console.log(to, from); const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
const isGuestRoute = to.matched.some((record) => record.meta.guest);
// // if (to.matched.some((record) => record.meta.requiresAuth)) { if (requiresAuth) {
// if (!isAuthenticated()) { if (!hasToken()) {
// next({ next({ path: "/login" });
// path: "/login", return;
// }); }
// return; const isValid = await validateToken();
// }
// // const requiredPermissions = to.meta.permissions || []; if (!isValid) {
redirectToLogin();
return;
}
// // if (requiredPermissions.length > 0 && !hasPermission(requiredPermissions)) { const requiredPermissions = to.meta.permissions || [];
// // next({ path: "/unauthorized" });
// // return; if (requiredPermissions.length > 0 && !hasPermission(requiredPermissions)) {
// // } next({ path: "/videos" });
// // } return;
}
// // if (to.matched.some((record) => record.meta.guest) && isAuthenticated()) { next();
// // next({ path: "/" }); return;
}
// // return; if (isGuestRoute && hasToken()) {
// // } const isValid = await validateToken();
// next(); if (isValid) {
// return; next({ path: "/videos" });
// }); return;
}
redirectToLogin();
return;
}
next();
});

View File

@@ -2,10 +2,7 @@ import axios from "axios";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
export const API = axios.create({ export const API = axios.create({
baseURL: "http://localhost:3000", baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000",
// process.env.NODE_ENV === "development"
// ? "https://api.clipperia.com.br"
// : "http://nestjs:3000",
}); });
API.interceptors.request.use((config) => { API.interceptors.request.use((config) => {
@@ -21,7 +18,7 @@ API.interceptors.request.use((config) => {
API.interceptors.response.use( API.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
if (error.response.status === 401) { if (error.response?.status === 401) {
Cookies.remove("token"); Cookies.remove("token");
window.location.href = "/login"; window.location.href = "/login";
} }

View File

@@ -68,6 +68,8 @@
:options="situations" :options="situations"
:loading="situationsLoading" :loading="situationsLoading"
multiple multiple
emit-value
map-options
/> />
</div> </div>
@@ -232,6 +234,9 @@ export default {
page: pagination.page, page: pagination.page,
...baseParams, ...baseParams,
}, },
paramsSerializer: {
indexes: null,
},
}); });
this.rows = data.content; this.rows = data.content;

47
src/utils/auth.js Normal file
View File

@@ -0,0 +1,47 @@
import Cookies from "js-cookie";
import { API } from "../config/axios";
/**
* Checks if the user has a valid session token
* @returns {boolean} True if token cookie exists
*/
export function hasToken() {
const token = Cookies.get("token");
return !!token;
}
/**
* Validates the current token by calling the backend /check-token endpoint
* @returns {Promise<boolean>} True if token is valid, false otherwise
*/
export async function validateToken() {
if (!hasToken()) {
return false;
}
try {
const response = await API.get("/auth/check-token");
return response.data?.valid === true;
} catch (error) {
// If the request fails (401, 403, network error, etc.), token is invalid
return false;
}
}
/**
* Clears all authentication data from cookies
*/
export function clearAuthData() {
Cookies.remove("token");
Cookies.remove("refresh_token");
Cookies.remove("user_roles");
Cookies.remove("user_profile");
}
/**
* Redirects to login page and clears auth data
*/
export function redirectToLogin() {
clearAuthData();
window.location.href = "/login";
}