Merge branch 'feat' of https://gitea.clipperia.com.br/admin/clipperia into feat
This commit is contained in:
@@ -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
6
.env.example
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
22
nginx.conf
22
nginx.conf
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
47
src/utils/auth.js
Normal 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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user