Compare commits

..

No commits in common. "main" and "newRemoteUI" have entirely different histories.

11 changed files with 58 additions and 360 deletions

View File

@ -1,93 +0,0 @@
# Frontend Guide - Microsoft SSO (Entra ID)
## 1) Muc tieu
Tai lieu nay danh cho frontend de tich hop dang nhap Microsoft SSO voi backend TTMT.CompManageWeb.
## 2) Redirect URI da dang ky
Redirect URI cho Microsoft app:
https://comp.soict.io/api/auth/sso/callback
Luu y:
- Redirect URI trong Azure App Registration phai giong 100% (scheme, domain, path).
- Sai ky tu hoac sai slash cuoi co the gay loi redirect_uri mismatch.
## 3) Endpoint frontend can goi
Backend route SSO Microsoft (legacy route):
- GET /api/auth/sso/login?returnUrl={FRONTEND_RETURN_URL}
- POST /api/auth/sso/exchange
Route alias cung ho tro:
- GET /api/sso/login?returnUrl={FRONTEND_RETURN_URL}
- POST /api/sso/exchange
## 4) Login flow cho frontend
1. User bam nut "Login with Microsoft".
2. Frontend redirect browser den:
/api/auth/sso/login?returnUrl={FRONTEND_RETURN_URL}
3. Backend redirect sang Microsoft login page.
4. Sau khi user xac thuc thanh cong, Microsoft goi ve redirect URI:
https://comp.soict.io/api/auth/sso/callback
5. Backend tao one-time code va redirect user ve FRONTEND_RETURN_URL kem query:
?code={ONE_TIME_CODE}
6. Frontend doc code trong URL, goi API exchange de doi code lay JWT noi bo.
7. Frontend luu token va thong tin user, dieu huong vao app.
## 5) API exchange chi tiet
Method: POST
Path: /api/auth/sso/exchange
Content-Type: application/json
Request body:
{
"code": "<one-time-code>"
}
Success response (HTTP 200):
{
"token": "<jwt>",
"name": "Nguyen Van A",
"username": "user@hust.edu.vn",
"access": [1, 2, 3],
"role": {
"roleName": "Pending",
"priority": 99
}
}
## 6) Error handling goi y cho frontend
- 400 BadRequest: code thieu hoac token khong hop le.
- 401 Unauthorized: code het han / da dung / khong hop le.
- 404 NotFound: user khong ton tai sau khi exchange.
One-time code co han su dung ngan (khoang 2 phut) va chi dung 1 lan.
Neu exchange that bai, can yeu cau user login lai.
## 7) Mau frontend pseudo-code
const returnUrl = window.location.origin + "/sso/complete";
window.location.href = `/api/auth/sso/login?returnUrl=${encodeURIComponent(returnUrl)}`;
// tai /sso/complete
const code = new URLSearchParams(window.location.search).get("code");
if (code) {
const res = await fetch("/api/auth/sso/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code })
});
if (!res.ok) {
// hien thi thong bao loi va cho user thu lai
} else {
const data = await res.json();
// save data.token, data.username, data.role, data.access
}
}
## 8) Checklist truoc khi UAT
- Redirect URI trong Azure dung: https://comp.soict.io/api/auth/sso/callback
- Frontend returnUrl la URL FE hop le (vi du: https://comp.soict.io/sso/complete)
- Frontend parse duoc query code
- Frontend goi exchange ngay sau khi nhan code
- Frontend xu ly day du HTTP 400/401/404

View File

@ -3,7 +3,6 @@
# server 127.0.0.1:8080; # server 127.0.0.1:8080;
# server 172.18.10.8:8080; # server 172.18.10.8:8080;
# } # }
server { server {
listen 80; listen 80;
server_name comp.soict.io; server_name comp.soict.io;
@ -13,197 +12,82 @@ server {
} }
location / { location / {
# root /usr/share/nginx/html;
# index index.html index.htm;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
} }
server { server{
listen 443 ssl; listen 443 ssl;
server_name comp.soict.io; server_name comp.soict.io;
ssl_certificate /etc/letsencrypt/live/comp.soict.io/fullchain.pem; ssl_certificate /etc/letsencrypt/live/comp.soict.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/comp.soict.io/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/comp.soict.io/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_ciphers HIGH:!aNULL:!MD5;
# MeshCentral proxied flow can set sizable auth cookies. set $backend_server 172.18.10.8:8080;
client_header_buffer_size 16k;
large_client_header_buffers 8 32k;
# Required when proxy_pass uses variables.
# In Docker, 127.0.0.11 is the embedded DNS resolver.
resolver 127.0.0.11 valid=30s ipv6=off;
resolver_timeout 5s;
set $backend_server ttmt-web:8080;
# Internal MeshCentral hop to avoid upstream TLS handshake instability.
set $meshserver meshcentral:8082;
# Public host MeshCentral expects in Host header.
set $meshhost soict-overleaf.tailc51e09.ts.net;
root /usr/share/nginx/html; root /usr/share/nginx/html;
# Default file to serve for directory requests
index index.html index.htm; index index.html index.htm;
# MeshCentral auth entrypoint. If iframe/browser lands on /login due to
# redirect, keep it on MeshCentral instead of frontend routing.
location = /login {
proxy_pass http://$meshserver;
proxy_http_version 1.1;
proxy_set_header Host $meshhost;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_cookie_path / "/; HTTPOnly; Secure; SameSite=None";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
# MeshCentral may redirect to "/" with remote params after login.
# Detect those requests and proxy them to MeshCentral instead of SPA.
location = / {
if ($arg_node != "") {
rewrite ^ /__mesh_root_proxy__ last;
}
if ($arg_viewmode != "") {
rewrite ^ /__mesh_root_proxy__ last;
}
if ($arg_gotonode != "") {
rewrite ^ /__mesh_root_proxy__ last;
}
try_files $uri $uri/ /index.html;
}
location = /__mesh_root_proxy__ {
proxy_pass http://$meshserver;
proxy_http_version 1.1;
proxy_set_header Host $meshhost;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_cookie_path / "/; HTTPOnly; Secure; SameSite=None";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
location / { location / {
# Try to serve the requested file directly ($uri)
# If it's a directory, try serving the index file ($uri/)
# If neither exists, fall back to serving /index.html
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# Optional: Add cache control headers for static assets for better performance
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|webp)$ { location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|webp)$ {
expires 1y; expires 1y;
add_header Cache-Control "public"; add_header Cache-Control "public";
access_log off; access_log off; # Optional: Don't log accesses for static files
} }
location /api/ { location /api/ {
proxy_pass http://$backend_server; proxy_pass http://$backend_server;
client_max_body_size 900M; # Cho phép upload file lớn (vd: 200MB)
client_max_body_size 200M;
# Truyền thẳng stream sang backend
proxy_request_buffering off; proxy_request_buffering off;
# Tăng timeout khi upload
proxy_read_timeout 300s; proxy_read_timeout 300s;
proxy_connect_timeout 300s; proxy_connect_timeout 300s;
proxy_send_timeout 300s; proxy_send_timeout 300s;
# CORS headers - Comment vi da xu ly o backend C#
# add_header 'Access-Control-Allow-Origin' '*' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = OPTIONS) { if ($request_method = OPTIONS) {
return 204; return 204;
} }
} }
location /api/Sse/events { location /api/Sse/events {
proxy_pass http://$backend_server/api/Sse/events; proxy_pass http://$backend_server/api/Sse/events;
proxy_http_version 1.1; proxy_http_version 1.1;
# cần thiết cho SSE
proxy_set_header Connection ''; proxy_set_header Connection '';
proxy_buffering off; proxy_buffering off;
proxy_cache off; proxy_cache off;
proxy_read_timeout 1h; proxy_read_timeout 1h;
} }
# MeshCentral client builds WebSocket URL from current location, location /mesh-proxy/ {
# e.g. wss://comp.soict.io/control.ashx. proxy_pass https://202.191.59.59/;
location ~ ^/(control|meshrelay|commander|mesh)\.ashx$ {
proxy_pass http://$meshserver;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $meshhost;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
location = /api/meshcentral/proxy {
return 301 /api/meshcentral/proxy/;
}
location ^~ /api/meshcentral/proxy/ {
# Forward directly to MeshCentral, but strip proxy prefix first.
# Without this, upstream sees /api/meshcentral/proxy/* and can redirect-loop.
rewrite ^/api/meshcentral/proxy/(.*)$ /$1 break;
proxy_pass http://$meshserver;
proxy_cookie_path / "/; HTTPOnly; Secure; SameSite=None"; proxy_cookie_path / "/; HTTPOnly; Secure; SameSite=None";
proxy_set_header Host $meshhost; # Cấu hình WebSocket cho commander.ashx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# Keep browser navigation under /api/meshcentral/proxy/*.
proxy_redirect ~^https?://[^/]+(/.*)$ /api/meshcentral/proxy$1;
proxy_redirect ~^(/.*)$ /api/meshcentral/proxy$1;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# FE production currently builds mesh proxy path as /meshapi/api/meshcentral/proxy/...
location = /meshapi/api/meshcentral/proxy {
return 301 /meshapi/api/meshcentral/proxy/;
}
location ^~ /meshapi/api/meshcentral/proxy/ {
# Legacy frontend path -> backend MeshCentralProxyController
rewrite ^/meshapi/api/meshcentral/proxy/(.*)$ /$1 break;
proxy_pass http://$backend_server;
proxy_cookie_path / "/; HTTPOnly; Secure; SameSite=None";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
} }
} }

View File

@ -8,7 +8,7 @@ import { useGetClientFolderStatusForDevice } from "@/hooks/queries";
import type { ClientFolderStatus } from "@/types/folder"; import type { ClientFolderStatus } from "@/types/folder";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { getRemoteDesktopUrl } from "@/services/remote-control.service"; import { getRemoteDesktopUrl } from "@/services/remote-control.service";
import { buildMeshProxyUrl } from "@/config/api"; import { BASE_URL } from "@/config/api";
import { toast } from "sonner"; import { toast } from "sonner";
export function ComputerCard({ export function ComputerCard({
device, device,
@ -52,7 +52,11 @@ export function ComputerCard({
const response = await getRemoteDesktopUrl(device.id); const response = await getRemoteDesktopUrl(device.id);
const originalUrl = new URL(response.url); const originalUrl = new URL(response.url);
const pathAndQuery = originalUrl.pathname + originalUrl.search; const pathAndQuery = originalUrl.pathname + originalUrl.search;
const proxyUrlFull = buildMeshProxyUrl(pathAndQuery); const cleanPath = pathAndQuery.startsWith("/")
? pathAndQuery.substring(1)
: pathAndQuery;
const baseWithoutApi = BASE_URL.replace("/api", "");
const proxyUrlFull = `${baseWithoutApi}/api/meshcentral/proxy/${cleanPath}`;
setProxyUrl(proxyUrlFull); setProxyUrl(proxyUrlFull);
setShowRemote(true); setShowRemote(true);

View File

@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import type { LoginResquest } from "@/types/auth"; import type { LoginResquest } from "@/types/auth";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { buildGoogleOAuthLoginUrl, buildMicrosoftSsoLoginUrl, login } from "@/services/auth.service"; import { buildGoogleOAuthLoginUrl, login } from "@/services/auth.service";
import { useState } from "react"; import { useState } from "react";
import { useNavigate, useRouter } from "@tanstack/react-router"; import { useNavigate, useRouter } from "@tanstack/react-router";
import { Route } from "@/routes/(auth)/login"; import { Route } from "@/routes/(auth)/login";
@ -46,22 +46,12 @@ export function LoginForm({ className }: React.ComponentProps<"form">) {
const handleGoogleLogin = () => { const handleGoogleLogin = () => {
const returnUrl = new URL("/oauth/callback", window.location.origin); const returnUrl = new URL("/oauth/callback", window.location.origin);
returnUrl.searchParams.set("provider", "google");
if (search.redirect) { if (search.redirect) {
returnUrl.searchParams.set("redirect", search.redirect); returnUrl.searchParams.set("redirect", search.redirect);
} }
window.location.assign(buildGoogleOAuthLoginUrl(returnUrl.toString())); window.location.assign(buildGoogleOAuthLoginUrl(returnUrl.toString()));
}; };
const handleMicrosoftLogin = () => {
const returnUrl = new URL("/oauth/callback", window.location.origin);
returnUrl.searchParams.set("provider", "azuread");
if (search.redirect) {
returnUrl.searchParams.set("redirect", search.redirect);
}
window.location.assign(buildMicrosoftSsoLoginUrl(returnUrl.toString()));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setErrorMessage(null); setErrorMessage(null);
@ -143,15 +133,6 @@ export function LoginForm({ className }: React.ComponentProps<"form">) {
</svg> </svg>
Đăng nhập với Google Đăng nhập với Google
</Button> </Button>
<Button type="button" variant="outline" className="w-full gap-2" onClick={handleMicrosoftLogin}>
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4">
<rect x="2" y="2" width="9" height="9" fill="#F35325" />
<rect x="13" y="2" width="9" height="9" fill="#81BC06" />
<rect x="2" y="13" width="9" height="9" fill="#05A6F0" />
<rect x="13" y="13" width="9" height="9" fill="#FFBA08" />
</svg>
Đăng nhập với Microsoft
</Button>
</div> </div>
</form> </form>
</CardContent> </CardContent>

View File

@ -18,7 +18,7 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr
const [isDone, setIsDone] = useState(false); const [isDone, setIsDone] = useState(false);
// Match server allowed extensions // Match server allowed extensions
const ALLOWED_EXTENSIONS = [".exe", ".apk", ".conf", ".json", ".xml", ".setting", ".lnk", ".url", ".seb", ".ps1", ".zip"]; const ALLOWED_EXTENSIONS = [".exe", ".apk", ".conf", ".json", ".xml", ".setting", ".lnk", ".url", ".seb", ".ps1"];
const isFileValid = (file: File) => { const isFileValid = (file: File) => {
const fileName = file.name.toLowerCase(); const fileName = file.name.toLowerCase();
return ALLOWED_EXTENSIONS.some((ext) => fileName.endsWith(ext)); return ALLOWED_EXTENSIONS.some((ext) => fileName.endsWith(ext));

View File

@ -1,39 +1,12 @@
const isDev = import.meta.env.MODE === "development"; const isDev = import.meta.env.MODE === "development";
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "");
export const BASE_URL = isDev export const BASE_URL = isDev
? import.meta.env.VITE_API_URL_DEV ? import.meta.env.VITE_API_URL_DEV
: "/api"; : "/api";
export const BASE_MESH_URL = isDev export const BASE_MESH_URL = isDev
? (import.meta.env.VITE_API_MESH || import.meta.env.VITE_API_MESH_DEV || "") ? import.meta.env.VITE_API_MESH_DEV
: (import.meta.env.VITE_API_MESH || ""); : "/meshapi";
export const buildMeshProxyUrl = (meshPathAndQuery: string) => {
const cleanPath = meshPathAndQuery.startsWith("/")
? meshPathAndQuery.substring(1)
: meshPathAndQuery;
const proxyPath = `/api/meshcentral/proxy/${cleanPath}`;
// If an explicit mesh host is configured, always use it.
// This allows forcing proxy URLs to https://<IP>:<port>/api/meshcentral/proxy/...
if (BASE_MESH_URL && BASE_MESH_URL.startsWith("http")) {
return `${trimTrailingSlash(BASE_MESH_URL)}${proxyPath}`;
}
// In development, BASE_URL is usually absolute (e.g. http://localhost:5218/api).
// Build an absolute proxy URL to backend so iframe requests do not hit Vite dev server.
if (BASE_URL.startsWith("http")) {
const apiBase = trimTrailingSlash(BASE_URL);
const backendOrigin = apiBase.endsWith("/api")
? apiBase.slice(0, -4)
: apiBase;
return `${backendOrigin}${proxyPath}`;
}
return proxyPath;
};
export const API_ENDPOINTS = { export const API_ENDPOINTS = {
AUTH: { AUTH: {

View File

@ -5,7 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { LoaderCircle } from "lucide-react"; import { LoaderCircle } from "lucide-react";
import axios from "axios"; import axios from "axios";
import { exchangeCodeByProvider } from "@/services/auth.service"; import { exchangeOAuthCode } from "@/services/auth.service";
import type { LoginResponse } from "@/types/auth"; import type { LoginResponse } from "@/types/auth";
const inFlightExchanges = new Map<string, Promise<LoginResponse>>(); const inFlightExchanges = new Map<string, Promise<LoginResponse>>();
@ -18,21 +18,18 @@ export const Route = createFileRoute("/(auth)/oauth/callback/")({
function OAuthCallbackPage() { function OAuthCallbackPage() {
const auth = useAuth(); const auth = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const search = Route.useSearch() as { code?: string; redirect?: string; provider?: string }; const search = Route.useSearch() as { code?: string; redirect?: string };
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isExchanging, setIsExchanging] = useState(false); const [isExchanging, setIsExchanging] = useState(false);
useEffect(() => { useEffect(() => {
const code = search.code; const code = search.code;
const provider = (search.provider || "").toLowerCase();
if (!code) { if (!code) {
setErrorMessage("OAuth code is missing."); setErrorMessage("OAuth code is missing.");
return; return;
} }
const exchangeId = `${provider || "auto"}:${code}`; if (consumedCodes.has(code)) {
if (consumedCodes.has(exchangeId)) {
setErrorMessage("Mã đăng nhập đã được sử dụng. Vui lòng đăng nhập lại."); setErrorMessage("Mã đăng nhập đã được sử dụng. Vui lòng đăng nhập lại.");
return; return;
} }
@ -41,10 +38,10 @@ function OAuthCallbackPage() {
setIsExchanging(true); setIsExchanging(true);
let cancelled = false; let cancelled = false;
let exchangePromise = inFlightExchanges.get(exchangeId); let exchangePromise = inFlightExchanges.get(code);
if (!exchangePromise) { if (!exchangePromise) {
exchangePromise = exchangeCodeByProvider(code, provider); exchangePromise = exchangeOAuthCode(code);
inFlightExchanges.set(exchangeId, exchangePromise); inFlightExchanges.set(code, exchangePromise);
} }
exchangePromise exchangePromise
@ -56,7 +53,7 @@ function OAuthCallbackPage() {
return; return;
} }
consumedCodes.add(exchangeId); consumedCodes.add(code);
localStorage.setItem("token", data.token); localStorage.setItem("token", data.token);
localStorage.setItem("username", data.username || ""); localStorage.setItem("username", data.username || "");
@ -77,14 +74,14 @@ function OAuthCallbackPage() {
if (cancelled) return; if (cancelled) return;
if (axios.isAxiosError(error) && error.response?.status === 401) { if (axios.isAxiosError(error) && error.response?.status === 401) {
consumedCodes.add(exchangeId); consumedCodes.add(code);
setErrorMessage("Mã đăng nhập đã hết hạn hoặc đã được sử dụng. Vui lòng đăng nhập lại."); setErrorMessage("Mã đăng nhập đã hết hạn hoặc đã được sử dụng. Vui lòng đăng nhập lại.");
return; return;
} }
setErrorMessage("OAuth exchange failed."); setErrorMessage("OAuth exchange failed.");
}) })
.finally(() => { .finally(() => {
inFlightExchanges.delete(exchangeId); inFlightExchanges.delete(code);
if (!cancelled) { if (!cancelled) {
setIsExchanging(false); setIsExchanging(false);
} }
@ -93,7 +90,7 @@ function OAuthCallbackPage() {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [auth, navigate, search.code, search.provider, search.redirect]); }, [auth, navigate, search.code, search.redirect]);
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-background to-muted/20"> <div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-background to-muted/20">

View File

@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { getRemoteDesktopUrl } from "@/services/remote-control.service"; import { getRemoteDesktopUrl } from "@/services/remote-control.service";
import { buildMeshProxyUrl } from "@/config/api"; import { BASE_URL } from "@/config/api";
export const Route = createFileRoute("/_auth/remote-control/")({ export const Route = createFileRoute("/_auth/remote-control/")({
@ -38,7 +38,9 @@ function RemoteControlPage() {
// Chuyển URL MeshCentral thành proxy URL // Chuyển URL MeshCentral thành proxy URL
const originalUrl = new URL(data.url); const originalUrl = new URL(data.url);
const pathAndQuery = originalUrl.pathname + originalUrl.search; const pathAndQuery = originalUrl.pathname + originalUrl.search;
const proxyUrlFull = buildMeshProxyUrl(pathAndQuery); const cleanPath = pathAndQuery.startsWith('/') ? pathAndQuery.substring(1) : pathAndQuery;
const baseWithoutApi = BASE_URL.replace('/api', '');
const proxyUrlFull = `${baseWithoutApi}/api/meshcentral/proxy/${cleanPath}`;
console.log("[RemoteControl] Proxy URL:", proxyUrlFull); console.log("[RemoteControl] Proxy URL:", proxyUrlFull);
setProxyUrl(proxyUrlFull); setProxyUrl(proxyUrlFull);

View File

@ -36,25 +36,6 @@ function RouteComponent() {
roomName as string, roomName as string,
); );
const sortedFolderStatusList = useMemo(() => {
return [...(folderStatusList ?? [])].sort((a, b) => {
const aRoom = (a as ClientFolderStatus & { roomName?: string }).roomName;
const bRoom = (b as ClientFolderStatus & { roomName?: string }).roomName;
if (aRoom || bRoom) {
return (aRoom ?? "").localeCompare(bRoom ?? "", "vi", {
numeric: true,
sensitivity: "base",
});
}
return (a.deviceId ?? "").localeCompare(b.deviceId ?? "", "vi", {
numeric: true,
sensitivity: "base",
});
});
}, [folderStatusList]);
const columnHelper = createColumnHelper<ClientFolderStatus>(); const columnHelper = createColumnHelper<ClientFolderStatus>();
const columns = useMemo( const columns = useMemo(
@ -99,7 +80,7 @@ function RouteComponent() {
); );
const table = useReactTable({ const table = useReactTable({
data: sortedFolderStatusList, data: folderStatusList ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
@ -107,7 +88,7 @@ function RouteComponent() {
return ( return (
<FolderStatusTemplate <FolderStatusTemplate
roomName={roomName as string} roomName={roomName as string}
data={sortedFolderStatusList} data={folderStatusList}
isLoading={isLoading} isLoading={isLoading}
onBack={() => onBack={() =>
navigate({ to: "/rooms/$roomName/", params: { roomName } } as any) navigate({ to: "/rooms/$roomName/", params: { roomName } } as any)

View File

@ -35,16 +35,6 @@ export function buildGoogleOAuthLoginUrl(returnUrl: string): string {
return buildOAuthLoginUrl("google", returnUrl); return buildOAuthLoginUrl("google", returnUrl);
} }
/**
* Build Microsoft SSO login URL (legacy endpoint)
* @param returnUrl - FE callback url
*/
export function buildMicrosoftSsoLoginUrl(returnUrl: string): string {
const base = API_ENDPOINTS.AUTH.SSO_LOGIN;
const encoded = encodeURIComponent(returnUrl);
return `${base}?returnUrl=${encoded}`;
}
/** /**
* Exchange one-time OAuth code for login payload * Exchange one-time OAuth code for login payload
* @param code - one-time code * @param code - one-time code
@ -85,27 +75,6 @@ export async function exchangeSsoCode(code: string): Promise<LoginResponse> {
return exchangeOAuthCode(code); return exchangeOAuthCode(code);
} }
/**
* Exchange one-time code by provider without breaking existing flows.
* - azuread/microsoft: force legacy SSO exchange endpoint
* - default: use OAuth exchange flow
*/
export async function exchangeCodeByProvider(
code: string,
provider?: string
): Promise<LoginResponse> {
const providerKey = (provider || "").toLowerCase();
if (providerKey === "microsoft" || providerKey === "azuread") {
const response = await rawAxios.post<LoginResponse>(
API_ENDPOINTS.AUTH.SSO_EXCHANGE,
{ code }
);
return response.data;
}
return exchangeOAuthCode(code);
}
/** /**
* Đăng xuất * Đăng xuất
*/ */

View File

@ -4,7 +4,7 @@ export type UserProfile = {
name: string; name: string;
role: string; role: string;
roleId: number; roleId: number;
accessRooms: string[]; accessRooms: number[];
createdAt?: string | null; createdAt?: string | null;
createdBy?: string | null; createdBy?: string | null;
updatedAt?: string | null; updatedAt?: string | null;