feat: Update dashboard and identity-gateway infrastructure
- Add Tenants module to Identity Gateway - Update Dashboard Auth context and components - Refactor token service and user/role controllers - Add Quadlet container definitions for dev environment
This commit is contained in:
parent
3a236a250a
commit
ebb405c4e4
30 changed files with 4641 additions and 76 deletions
23
core-dashboard-dev.container
Normal file
23
core-dashboard-dev.container
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Core Dashboard (Ambiente Dev)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
Environment=REGISTRY_AUTH_FILE=/run/user/0/containers/auth.json
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=rg.fr-par.scw.cloud/yumi/dashboard:latest
|
||||||
|
ContainerName=core-dashboard-dev
|
||||||
|
Pull=always
|
||||||
|
EnvironmentFile=/mnt/data/core/dashboard/.env
|
||||||
|
PublishPort=3005:3000
|
||||||
|
Network=web_proxy
|
||||||
|
Label=traefik.enable=true
|
||||||
|
Label=traefik.http.routers.core-dashboard-dev.rule=Host(`dev.rede5.com.br`)
|
||||||
|
Label=traefik.http.routers.core-dashboard-dev.entrypoints=websecure
|
||||||
|
Label=traefik.http.routers.core-dashboard-dev.tls.certresolver=myresolver
|
||||||
|
Label=traefik.http.services.core-dashboard-dev.loadbalancer.server.port=3000
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
24
core-identity-gateway-dev.container
Normal file
24
core-identity-gateway-dev.container
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Core Identity Gateway (Ambiente Dev)
|
||||||
|
After=network-online.target postgres.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
Environment=REGISTRY_AUTH_FILE=/run/user/0/containers/auth.json
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=rg.fr-par.scw.cloud/yumi/identity-gateway:latest
|
||||||
|
ContainerName=core-identity-gateway-dev
|
||||||
|
Pull=always
|
||||||
|
EnvironmentFile=/mnt/data/core/identity-gateway/.env
|
||||||
|
Environment=DATABASE_URL=postgres://identity:identity@postgres-main:5432/identity_gateway
|
||||||
|
PublishPort=4005:4000
|
||||||
|
Network=web_proxy
|
||||||
|
Label=traefik.enable=true
|
||||||
|
Label=traefik.http.routers.core-identity-gateway-dev.rule=Host(`ig-dev.rede5.com.br`)
|
||||||
|
Label=traefik.http.routers.core-identity-gateway-dev.entrypoints=websecure
|
||||||
|
Label=traefik.http.routers.core-identity-gateway-dev.tls.certresolver=myresolver
|
||||||
|
Label=traefik.http.services.core-identity-gateway-dev.loadbalancer.server.port=4000
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
12
dashboard/.env.example
Normal file
12
dashboard/.env.example
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Identity Gateway
|
||||||
|
VITE_IDENTITY_GATEWAY_URL=https://ig-dev.rede5.com.br
|
||||||
|
VITE_TENANT_ID=your-tenant-id-here
|
||||||
|
|
||||||
|
# Appwrite (legacy - can be removed)
|
||||||
|
VITE_APPWRITE_ENDPOINT=
|
||||||
|
VITE_APPWRITE_PROJECT_ID=
|
||||||
|
VITE_APPWRITE_DATABASE_ID=
|
||||||
|
VITE_APPWRITE_COLLECTION_SERVERS_ID=
|
||||||
|
VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID=
|
||||||
|
VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID=
|
||||||
|
VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID=
|
||||||
|
|
@ -10,8 +10,15 @@ RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
# Stage 2: Serve with Distroless Node.js
|
||||||
|
FROM gcr.io/distroless/nodejs20-debian12
|
||||||
|
|
||||||
EXPOSE 80
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
COPY --from=builder /app/dist ./dist
|
||||||
|
COPY --from=builder /app/server.js ./server.js
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["server.js"]
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export function TerminalLogs() {
|
||||||
const logTitle = useMemo(() => (open ? 'Ocultar Terminal' : 'Mostrar Terminal'), [open])
|
const logTitle = useMemo(() => (open ? 'Ocultar Terminal' : 'Mostrar Terminal'), [open])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!appwriteDatabaseId || !appwriteCollectionAuditLogsId) return undefined
|
if (!appwriteDatabaseId || !appwriteCollectionAuditLogsId || !client) return undefined
|
||||||
|
|
||||||
const channel = `databases.${appwriteDatabaseId}.collections.${appwriteCollectionAuditLogsId}.documents`
|
const channel = `databases.${appwriteDatabaseId}.collections.${appwriteCollectionAuditLogsId}.documents`
|
||||||
const unsubscribe = client.subscribe(channel, (response) => {
|
const unsubscribe = client.subscribe(channel, (response) => {
|
||||||
|
|
|
||||||
|
|
@ -31,22 +31,16 @@ export default function UserDropdown() {
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInitials = (name?: string, email?: string) => {
|
const getInitials = (identifier?: string) => {
|
||||||
if (name) {
|
if (identifier) {
|
||||||
const names = name.split(' ')
|
return identifier.substring(0, 2).toUpperCase()
|
||||||
return names.length >= 2
|
|
||||||
? `${names[0][0]}${names[names.length - 1][0]}`.toUpperCase()
|
|
||||||
: name.substring(0, 2).toUpperCase()
|
|
||||||
}
|
|
||||||
if (email) {
|
|
||||||
return email.substring(0, 2).toUpperCase()
|
|
||||||
}
|
}
|
||||||
return 'U'
|
return 'U'
|
||||||
}
|
}
|
||||||
|
|
||||||
const initials = getInitials(user?.name, user?.email)
|
const initials = getInitials(user?.identifier)
|
||||||
const displayName = user?.name || user?.email?.split('@')[0] || 'Usuário'
|
const displayName = user?.identifier?.split('@')[0] || 'Usuário'
|
||||||
const displayEmail = user?.email || ''
|
const displayEmail = user?.identifier || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { AppwriteException, Models } from 'appwrite'
|
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
import { Navigate, Outlet, useLocation } from 'react-router-dom'
|
import { Navigate, Outlet, useLocation } from 'react-router-dom'
|
||||||
import { account } from '../lib/appwrite'
|
import { getCurrentUser, loginUser, logoutUser, User } from '../lib/auth'
|
||||||
|
|
||||||
type AuthContextValue = {
|
type AuthContextValue = {
|
||||||
user: Models.User<Models.Preferences> | null
|
user: User | null
|
||||||
loading: boolean
|
loading: boolean
|
||||||
login: (email: string, password: string) => Promise<void>
|
login: (email: string, password: string) => Promise<void>
|
||||||
logout: () => Promise<void>
|
logout: () => Promise<void>
|
||||||
|
|
@ -14,21 +13,17 @@ type AuthContextValue = {
|
||||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined)
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined)
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null)
|
const [user, setUser] = useState<User | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const fetchUser = useCallback(async () => {
|
const fetchUser = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const current = await account.get()
|
const current = await getCurrentUser()
|
||||||
setUser(current)
|
setUser(current)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const authError = error as AppwriteException
|
|
||||||
if (authError?.code === 401) {
|
|
||||||
setUser(null)
|
|
||||||
} else {
|
|
||||||
console.error('Erro ao buscar usuário atual', error)
|
console.error('Erro ao buscar usuário atual', error)
|
||||||
}
|
setUser(null)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
@ -40,14 +35,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||||
|
|
||||||
const login = useCallback(
|
const login = useCallback(
|
||||||
async (email: string, password: string) => {
|
async (email: string, password: string) => {
|
||||||
await account.createEmailPasswordSession(email, password)
|
await loginUser(email, password)
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
},
|
},
|
||||||
[fetchUser],
|
[fetchUser],
|
||||||
)
|
)
|
||||||
|
|
||||||
const logout = useCallback(async () => {
|
const logout = useCallback(async () => {
|
||||||
await account.deleteSession('current')
|
await logoutUser()
|
||||||
setUser(null)
|
setUser(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export default function DashboardLayout() {
|
||||||
|
|
||||||
<div className="mt-auto space-y-2 border-t border-slate-800 pt-3 text-sm text-slate-300">
|
<div className="mt-auto space-y-2 border-t border-slate-800 pt-3 text-sm text-slate-300">
|
||||||
<p className="flex items-center justify-between text-xs text-slate-500">
|
<p className="flex items-center justify-between text-xs text-slate-500">
|
||||||
<span className="truncate">{user?.email}</span>
|
<span className="truncate">{user?.identifier}</span>
|
||||||
<Terminal size={14} className="text-cyan-300" />
|
<Terminal size={14} className="text-cyan-300" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
import { Account, AppwriteException, Client, Databases, Functions, Models } from 'appwrite'
|
import { Account, AppwriteException, Client, Databases, Functions, Models } from 'appwrite'
|
||||||
|
|
||||||
const appwriteEndpoint = import.meta.env.VITE_APPWRITE_ENDPOINT
|
const appwriteEndpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || ''
|
||||||
const appwriteProjectId = import.meta.env.VITE_APPWRITE_PROJECT_ID
|
const appwriteProjectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || ''
|
||||||
const appwriteDatabaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID
|
const appwriteDatabaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID
|
||||||
const appwriteCollectionServersId = import.meta.env.VITE_APPWRITE_COLLECTION_SERVERS_ID
|
const appwriteCollectionServersId = import.meta.env.VITE_APPWRITE_COLLECTION_SERVERS_ID
|
||||||
const appwriteCollectionGithubReposId = import.meta.env.VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID
|
const appwriteCollectionGithubReposId = import.meta.env.VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID
|
||||||
const appwriteCollectionAuditLogsId = import.meta.env.VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID
|
const appwriteCollectionAuditLogsId = import.meta.env.VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID
|
||||||
const appwriteCollectionCloudflareAccountsId = import.meta.env.VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID
|
const appwriteCollectionCloudflareAccountsId = import.meta.env.VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID
|
||||||
|
|
||||||
if (!appwriteEndpoint || !appwriteProjectId) {
|
// Appwrite is now optional - Identity Gateway is the primary auth provider
|
||||||
throw new Error('Defina VITE_APPWRITE_ENDPOINT e VITE_APPWRITE_PROJECT_ID no ambiente do Vite.')
|
const client = appwriteEndpoint && appwriteProjectId
|
||||||
}
|
? new Client().setEndpoint(appwriteEndpoint).setProject(appwriteProjectId)
|
||||||
|
: null
|
||||||
|
|
||||||
const client = new Client().setEndpoint(appwriteEndpoint).setProject(appwriteProjectId)
|
const account = client ? new Account(client) : null
|
||||||
|
const databases = client ? new Databases(client) : null
|
||||||
const account = new Account(client)
|
const functions = client ? new Functions(client) : null
|
||||||
const databases = new Databases(client)
|
|
||||||
const functions = new Functions(client)
|
|
||||||
|
|
||||||
const isSessionExpired = (error: unknown) => {
|
const isSessionExpired = (error: unknown) => {
|
||||||
if (error instanceof AppwriteException) {
|
if (error instanceof AppwriteException) {
|
||||||
|
|
@ -28,10 +27,12 @@ const isSessionExpired = (error: unknown) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loginUser = async (email: string, password: string) => {
|
export const loginUser = async (email: string, password: string) => {
|
||||||
|
if (!account) throw new Error('Appwrite not configured')
|
||||||
return account.createEmailPasswordSession(email, password)
|
return account.createEmailPasswordSession(email, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logoutUser = async () => {
|
export const logoutUser = async () => {
|
||||||
|
if (!account) return
|
||||||
try {
|
try {
|
||||||
await account.deleteSession('current')
|
await account.deleteSession('current')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -42,6 +43,7 @@ export const logoutUser = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCurrentUser = async (): Promise<Models.User<Models.Preferences> | null> => {
|
export const getCurrentUser = async (): Promise<Models.User<Models.Preferences> | null> => {
|
||||||
|
if (!account) return null
|
||||||
try {
|
try {
|
||||||
return await account.get()
|
return await account.get()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
142
dashboard/src/lib/auth.ts
Normal file
142
dashboard/src/lib/auth.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
const API_URL = import.meta.env.VITE_IDENTITY_GATEWAY_URL || "https://ig-dev.rede5.com.br";
|
||||||
|
// Fallback to Rede5 dev tenant ID if env var is missing
|
||||||
|
const TENANT_ID = import.meta.env.VITE_TENANT_ID || "aff0064a-6797-421f-af0f-832438608a95";
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
identifier: string;
|
||||||
|
status: string;
|
||||||
|
roles?: string[];
|
||||||
|
permissions?: string[];
|
||||||
|
tenantId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accessToken: string | null = null;
|
||||||
|
|
||||||
|
export const getAccessToken = () => accessToken;
|
||||||
|
|
||||||
|
export const setAccessToken = (token: string | null) => {
|
||||||
|
accessToken = token;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginUser = async (email: string, password: string): Promise<LoginResponse> => {
|
||||||
|
const response = await fetch(`${API_URL}/auth/login`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
identifier: email,
|
||||||
|
secret: password,
|
||||||
|
tenantId: TENANT_ID,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.message || "Login failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
accessToken = data.accessToken;
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logoutUser = async (): Promise<void> => {
|
||||||
|
await fetch(`${API_URL}/auth/logout`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
accessToken = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshToken = async (): Promise<string | null> => {
|
||||||
|
const response = await fetch(`${API_URL}/auth/refresh`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
accessToken = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
accessToken = data.accessToken;
|
||||||
|
return data.accessToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurrentUser = async (): Promise<User | null> => {
|
||||||
|
if (!accessToken) {
|
||||||
|
// Try to refresh token first
|
||||||
|
const refreshed = await refreshToken();
|
||||||
|
if (!refreshed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_URL}/users/me`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Try refresh and retry
|
||||||
|
const refreshed = await refreshToken();
|
||||||
|
if (!refreshed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const retryResponse = await fetch(`${API_URL}/users/me`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!retryResponse.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return retryResponse.json();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper for authenticated API calls
|
||||||
|
export const authFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
|
||||||
|
const headers = {
|
||||||
|
...options.headers,
|
||||||
|
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = await fetch(url, { ...options, credentials: "include", headers });
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
const refreshed = await refreshToken();
|
||||||
|
if (refreshed) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
response = await fetch(url, { ...options, credentials: "include", headers });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
@ -11,3 +11,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
// Force rebuild Tue Dec 30 18:43:49 -03 2025
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,10 @@ export default function AccountsAdmin() {
|
||||||
|
|
||||||
const fetchAccounts = async () => {
|
const fetchAccounts = async () => {
|
||||||
try {
|
try {
|
||||||
if (!appwriteDatabaseId) return
|
if (!appwriteDatabaseId || !databases) {
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
const response = await databases.listDocuments<Account>(
|
const response = await databases.listDocuments<Account>(
|
||||||
appwriteDatabaseId,
|
appwriteDatabaseId,
|
||||||
COLLECTION_ID,
|
COLLECTION_ID,
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default function Cloudflare() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCredentials = async () => {
|
const loadCredentials = async () => {
|
||||||
if (!appwriteDatabaseId || !appwriteCollectionCloudflareAccountsId) return
|
if (!appwriteDatabaseId || !appwriteCollectionCloudflareAccountsId || !databases) return
|
||||||
try {
|
try {
|
||||||
const response = await databases.listDocuments(appwriteDatabaseId, appwriteCollectionCloudflareAccountsId, [
|
const response = await databases.listDocuments(appwriteDatabaseId, appwriteCollectionCloudflareAccountsId, [
|
||||||
Query.equal('provider', 'cloudflare'),
|
Query.equal('provider', 'cloudflare'),
|
||||||
|
|
@ -54,7 +54,7 @@ export default function Cloudflare() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadWorkers = async () => {
|
const loadWorkers = async () => {
|
||||||
if (!appwriteDatabaseId || !appwriteCollectionServersId) return
|
if (!appwriteDatabaseId || !appwriteCollectionServersId || !databases) return
|
||||||
try {
|
try {
|
||||||
const servers = await databases.listDocuments<Models.Document & { status?: string }>(
|
const servers = await databases.listDocuments<Models.Document & { status?: string }>(
|
||||||
appwriteDatabaseId,
|
appwriteDatabaseId,
|
||||||
|
|
@ -76,7 +76,7 @@ export default function Cloudflare() {
|
||||||
const handleSaveKey = async (event: FormEvent) => {
|
const handleSaveKey = async (event: FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setError(null)
|
setError(null)
|
||||||
if (!appwriteDatabaseId || !appwriteCollectionCloudflareAccountsId) {
|
if (!appwriteDatabaseId || !appwriteCollectionCloudflareAccountsId || !databases) {
|
||||||
setError('Configure VITE_APPWRITE_DATABASE_ID e VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID para salvar a chave de API.')
|
setError('Configure VITE_APPWRITE_DATABASE_ID e VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID para salvar a chave de API.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +103,10 @@ export default function Cloudflare() {
|
||||||
setError('Selecione ou cadastre uma credencial Cloudflare antes de consultar o status.')
|
setError('Selecione ou cadastre uma credencial Cloudflare antes de consultar o status.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!functions) {
|
||||||
|
setError('Appwrite functions not configured.')
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const executionResult = await functions.createExecution(
|
const executionResult = await functions.createExecution(
|
||||||
'check-cloudflare-status',
|
'check-cloudflare-status',
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ export default function Github() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
|
if (!functions) {
|
||||||
|
setError('Appwrite functions not configured.')
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const executionResult = await functions.createExecution(
|
const executionResult = await functions.createExecution(
|
||||||
'sync-github',
|
'sync-github',
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ export default function Hello() {
|
||||||
setError(null)
|
setError(null)
|
||||||
setMessage(null)
|
setMessage(null)
|
||||||
|
|
||||||
|
if (!functions) {
|
||||||
|
setError('Appwrite functions not configured.')
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const executionResult = await functions.createExecution(
|
const executionResult = await functions.createExecution(
|
||||||
'hello-world',
|
'hello-world',
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ export default function Home() {
|
||||||
|
|
||||||
const fetchMetrics = async () => {
|
const fetchMetrics = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!databases) {
|
||||||
|
setError('Appwrite not configured. Configure VITE_APPWRITE_* variables to enable metrics.')
|
||||||
|
return
|
||||||
|
}
|
||||||
const [repos, servers, deployments] = await Promise.all([
|
const [repos, servers, deployments] = await Promise.all([
|
||||||
databases.listDocuments<Repo>(appwriteDatabaseId!, appwriteCollectionGithubReposId!, [Query.limit(1)]),
|
databases.listDocuments<Repo>(appwriteDatabaseId!, appwriteCollectionGithubReposId!, [Query.limit(1)]),
|
||||||
databases.listDocuments<Server>(appwriteDatabaseId!, appwriteCollectionServersId!, [Query.limit(200)]),
|
databases.listDocuments<Server>(appwriteDatabaseId!, appwriteCollectionServersId!, [Query.limit(200)]),
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,22 @@ import { useAuth } from '../contexts/Auth'
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
const getInitials = (name?: string, email?: string) => {
|
const getInitials = (identifier?: string) => {
|
||||||
if (name) {
|
if (identifier) {
|
||||||
const names = name.split(' ')
|
// If it's an email, use first 2 chars before @
|
||||||
return names.length >= 2
|
const atIndex = identifier.indexOf('@')
|
||||||
? `${names[0][0]}${names[names.length - 1][0]}`.toUpperCase()
|
if (atIndex > 0) {
|
||||||
: name.substring(0, 2).toUpperCase()
|
return identifier.substring(0, 2).toUpperCase()
|
||||||
}
|
}
|
||||||
if (email) {
|
return identifier.substring(0, 2).toUpperCase()
|
||||||
return email.substring(0, 2).toUpperCase()
|
|
||||||
}
|
}
|
||||||
return 'U'
|
return 'U'
|
||||||
}
|
}
|
||||||
|
|
||||||
const initials = getInitials(user?.name, user?.email)
|
const initials = getInitials(user?.identifier)
|
||||||
const displayName = user?.name || 'Usuário'
|
const displayName = user?.identifier?.split('@')[0] || 'Usuário'
|
||||||
const displayEmail = user?.email || 'email@exemplo.com'
|
const displayEmail = user?.identifier || 'email@exemplo.com'
|
||||||
const createdAt = user?.$createdAt ? new Date(user.$createdAt).toLocaleDateString('pt-BR') : 'N/A'
|
const createdAt = 'N/A' // Identity Gateway doesn't return createdAt in /users/me
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -72,7 +71,7 @@ export default function Profile() {
|
||||||
<User size={18} className="text-cyan-400" />
|
<User size={18} className="text-cyan-400" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-xs text-slate-500">User ID</p>
|
<p className="text-xs text-slate-500">User ID</p>
|
||||||
<p className="truncate font-mono text-xs text-slate-200">{user?.$id || 'N/A'}</p>
|
<p className="truncate font-mono text-xs text-slate-200">{user?.id || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
3710
identity-gateway/package-lock.json
generated
Normal file
3710
identity-gateway/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -11,21 +11,24 @@
|
||||||
"lint": "eslint . --ext .ts"
|
"lint": "eslint . --ext .ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
"@fastify/cors": "^11.2.0",
|
||||||
|
"@fastify/swagger": "^9.6.1",
|
||||||
|
"@fastify/swagger-ui": "^5.2.3",
|
||||||
|
"ajv-formats": "^3.0.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^5.6.2",
|
||||||
"@fastify/cookie": "^9.3.1",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"pino": "^8.19.0"
|
"pino": "^8.19.0",
|
||||||
|
"pino-pretty": "^11.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/node": "^20.14.10",
|
|
||||||
"@types/pg": "^8.11.6",
|
"@types/pg": "^8.11.6",
|
||||||
"eslint": "^9.6.0",
|
"eslint": "^9.6.0",
|
||||||
"pino-pretty": "^11.2.2",
|
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export interface TokenPair {
|
||||||
export class TokenService {
|
export class TokenService {
|
||||||
signAccessToken(payload: TenantContext) {
|
signAccessToken(payload: TenantContext) {
|
||||||
return jwt.sign(payload, env.jwtAccessSecret, {
|
return jwt.sign(payload, env.jwtAccessSecret, {
|
||||||
expiresIn: env.jwtAccessTtl,
|
expiresIn: env.jwtAccessTtl as any,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ export class TokenService {
|
||||||
},
|
},
|
||||||
env.jwtRefreshSecret,
|
env.jwtRefreshSecret,
|
||||||
{
|
{
|
||||||
expiresIn: env.jwtRefreshTtl,
|
expiresIn: env.jwtRefreshTtl as any,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
85
identity-gateway/src/lib/seed.ts
Normal file
85
identity-gateway/src/lib/seed.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { db } from "./db";
|
||||||
|
import { hashSecret } from "./crypto";
|
||||||
|
|
||||||
|
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || "admin@rede5.com.br";
|
||||||
|
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "admin123";
|
||||||
|
const DEFAULT_TENANT_NAME = "Rede5";
|
||||||
|
|
||||||
|
export const seed = async () => {
|
||||||
|
console.log("[Seed] Checking if seed is needed...");
|
||||||
|
|
||||||
|
// Check if default tenant exists
|
||||||
|
const tenantResult = await db.query<{ id: string }>(
|
||||||
|
"SELECT id FROM tenants WHERE name = $1",
|
||||||
|
[DEFAULT_TENANT_NAME]
|
||||||
|
);
|
||||||
|
|
||||||
|
let tenantId: string;
|
||||||
|
|
||||||
|
if (tenantResult.rowCount === 0) {
|
||||||
|
console.log("[Seed] Creating default tenant...");
|
||||||
|
const newTenant = await db.query<{ id: string }>(
|
||||||
|
"INSERT INTO tenants (name) VALUES ($1) RETURNING id",
|
||||||
|
[DEFAULT_TENANT_NAME]
|
||||||
|
);
|
||||||
|
tenantId = newTenant.rows[0].id;
|
||||||
|
} else {
|
||||||
|
tenantId = tenantResult.rows[0].id;
|
||||||
|
console.log("[Seed] Default tenant already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if admin user exists
|
||||||
|
const userResult = await db.query<{ id: string }>(
|
||||||
|
"SELECT id FROM users WHERE identifier = $1",
|
||||||
|
[ADMIN_EMAIL]
|
||||||
|
);
|
||||||
|
|
||||||
|
let userId: string;
|
||||||
|
|
||||||
|
if (userResult.rowCount === 0) {
|
||||||
|
console.log("[Seed] Creating admin user...");
|
||||||
|
const passwordHash = await hashSecret(ADMIN_PASSWORD);
|
||||||
|
const newUser = await db.query<{ id: string }>(
|
||||||
|
"INSERT INTO users (identifier, password_hash, status) VALUES ($1, $2, 'active') RETURNING id",
|
||||||
|
[ADMIN_EMAIL, passwordHash]
|
||||||
|
);
|
||||||
|
userId = newUser.rows[0].id;
|
||||||
|
} else {
|
||||||
|
userId = userResult.rows[0].id;
|
||||||
|
console.log("[Seed] Admin user already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if admin role exists
|
||||||
|
const roleResult = await db.query<{ id: string }>(
|
||||||
|
"SELECT id FROM roles WHERE name = $1",
|
||||||
|
["admin"]
|
||||||
|
);
|
||||||
|
|
||||||
|
let roleId: string;
|
||||||
|
|
||||||
|
if (roleResult.rowCount === 0) {
|
||||||
|
console.log("[Seed] Creating admin role...");
|
||||||
|
const newRole = await db.query<{ id: string }>(
|
||||||
|
"INSERT INTO roles (name, description) VALUES ('admin', 'Administrator with full access') RETURNING id",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
roleId = newRole.rows[0].id;
|
||||||
|
} else {
|
||||||
|
roleId = roleResult.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link user to tenant
|
||||||
|
await db.query(
|
||||||
|
"INSERT INTO user_tenants (user_id, tenant_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||||
|
[userId, tenantId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assign admin role to user in tenant
|
||||||
|
await db.query(
|
||||||
|
"INSERT INTO user_roles (user_id, tenant_id, role_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
|
||||||
|
[userId, tenantId, roleId]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("[Seed] Seed completed successfully!");
|
||||||
|
console.log(`[Seed] Admin: ${ADMIN_EMAIL} | Tenant: ${DEFAULT_TENANT_NAME}`);
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import cookie from "@fastify/cookie";
|
import cookie from "@fastify/cookie";
|
||||||
|
import cors from "@fastify/cors";
|
||||||
|
import swagger from "@fastify/swagger";
|
||||||
|
import swaggerUi from "@fastify/swagger-ui";
|
||||||
import { assertEnv, env } from "./lib/env";
|
import { assertEnv, env } from "./lib/env";
|
||||||
import { logger } from "./lib/logger";
|
import { logger } from "./lib/logger";
|
||||||
|
import { seed } from "./lib/seed";
|
||||||
import { TokenService } from "./core/token.service";
|
import { TokenService } from "./core/token.service";
|
||||||
import { UserService } from "./modules/users/user.service";
|
import { UserService } from "./modules/users/user.service";
|
||||||
import { SessionService } from "./modules/sessions/session.service";
|
import { SessionService } from "./modules/sessions/session.service";
|
||||||
|
|
@ -10,16 +14,100 @@ import { LocalProvider } from "./modules/providers/local.provider";
|
||||||
import { AuthProvider } from "./modules/providers/provider.interface";
|
import { AuthProvider } from "./modules/providers/provider.interface";
|
||||||
import { registerAuthRoutes } from "./modules/auth/auth.controller";
|
import { registerAuthRoutes } from "./modules/auth/auth.controller";
|
||||||
import { registerUserRoutes } from "./modules/users/user.controller";
|
import { registerUserRoutes } from "./modules/users/user.controller";
|
||||||
|
import { TenantService } from "./modules/tenants/tenant.service";
|
||||||
|
import { registerTenantRoutes } from "./modules/tenants/tenant.controller";
|
||||||
|
import { RoleService } from "./modules/roles/role.service";
|
||||||
|
import { registerRoleRoutes } from "./modules/roles/role.controller";
|
||||||
|
|
||||||
|
const VERSION = "0.1.0";
|
||||||
|
|
||||||
const bootstrap = async () => {
|
const bootstrap = async () => {
|
||||||
assertEnv();
|
assertEnv();
|
||||||
|
|
||||||
const app = Fastify({ logger });
|
const app = Fastify({
|
||||||
|
logger: {
|
||||||
|
level: process.env.LOG_LEVEL || "info",
|
||||||
|
...(process.env.NODE_ENV !== "production" && {
|
||||||
|
transport: {
|
||||||
|
target: "pino-pretty",
|
||||||
|
options: { colorize: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// CORS - Allow dashboard to access identity gateway
|
||||||
|
await app.register(cors, {
|
||||||
|
origin: [
|
||||||
|
"https://dev.rede5.com.br",
|
||||||
|
"https://dashboard.rede5.com.br",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost:3000",
|
||||||
|
],
|
||||||
|
credentials: true,
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Swagger
|
||||||
|
await app.register(swagger, {
|
||||||
|
openapi: {
|
||||||
|
info: {
|
||||||
|
title: "Identity Gateway API",
|
||||||
|
description: "Internal identity gateway for multi-service SaaS platforms",
|
||||||
|
version: VERSION,
|
||||||
|
},
|
||||||
|
servers: [{ url: `https://ig-dev.rede5.com.br` }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.register(swaggerUi, {
|
||||||
|
routePrefix: "/docs",
|
||||||
|
uiConfig: {
|
||||||
|
docExpansion: "list",
|
||||||
|
deepLinking: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
app.register(cookie);
|
app.register(cookie);
|
||||||
|
|
||||||
|
// AJV Formats for UUID validation
|
||||||
|
const Ajv = require("ajv");
|
||||||
|
const ajv = new Ajv({
|
||||||
|
removeAdditional: true,
|
||||||
|
useDefaults: true,
|
||||||
|
coerceTypes: true,
|
||||||
|
allErrors: true,
|
||||||
|
});
|
||||||
|
require("ajv-formats")(ajv);
|
||||||
|
app.setValidatorCompiler(({ schema }) => {
|
||||||
|
return ajv.compile(schema);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Root endpoint
|
||||||
|
app.get("/", async (request) => {
|
||||||
|
const ip = (request.headers["x-forwarded-for"] as string)?.split(",")[0] ||
|
||||||
|
request.headers["x-real-ip"] ||
|
||||||
|
request.ip;
|
||||||
|
return {
|
||||||
|
message: "🔐 Identity Gateway is running!",
|
||||||
|
version: VERSION,
|
||||||
|
docs: "/docs",
|
||||||
|
health: "/health",
|
||||||
|
ip,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health endpoint
|
||||||
|
app.get("/health", async () => {
|
||||||
|
return { status: "ok", timestamp: new Date().toISOString() };
|
||||||
|
});
|
||||||
|
|
||||||
const tokenService = new TokenService();
|
const tokenService = new TokenService();
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
const sessionService = new SessionService();
|
const sessionService = new SessionService();
|
||||||
|
const tenantService = new TenantService();
|
||||||
|
const roleService = new RoleService();
|
||||||
|
|
||||||
const providers = new Map<string, AuthProvider>();
|
const providers = new Map<string, AuthProvider>();
|
||||||
providers.set("local", new LocalProvider(userService));
|
providers.set("local", new LocalProvider(userService));
|
||||||
|
|
@ -31,8 +119,13 @@ const bootstrap = async () => {
|
||||||
providers
|
providers
|
||||||
);
|
);
|
||||||
|
|
||||||
registerAuthRoutes(app, authService);
|
registerAuthRoutes(app as any, authService);
|
||||||
registerUserRoutes(app, userService, tokenService);
|
registerUserRoutes(app as any, userService, tokenService);
|
||||||
|
registerTenantRoutes(app as any, tenantService, tokenService);
|
||||||
|
registerRoleRoutes(app as any, roleService, tokenService);
|
||||||
|
|
||||||
|
// Run seed on startup
|
||||||
|
await seed();
|
||||||
|
|
||||||
await app.listen({ port: env.port, host: "0.0.0.0" });
|
await app.listen({ port: env.port, host: "0.0.0.0" });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,20 @@ import { AuthService } from "./auth.service";
|
||||||
import { env } from "../../lib/env";
|
import { env } from "../../lib/env";
|
||||||
|
|
||||||
export const registerAuthRoutes = (app: FastifyInstance, authService: AuthService) => {
|
export const registerAuthRoutes = (app: FastifyInstance, authService: AuthService) => {
|
||||||
app.post("/auth/login", async (request, reply) => {
|
app.post("/auth/login", {
|
||||||
|
schema: {
|
||||||
|
body: {
|
||||||
|
type: "object",
|
||||||
|
required: ["identifier", "secret", "tenantId"],
|
||||||
|
properties: {
|
||||||
|
provider: { type: "string" },
|
||||||
|
identifier: { type: "string" },
|
||||||
|
secret: { type: "string" },
|
||||||
|
tenantId: { type: "string", format: "uuid" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, async (request, reply) => {
|
||||||
const body = request.body as {
|
const body = request.body as {
|
||||||
provider?: string;
|
provider?: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
|
|
|
||||||
94
identity-gateway/src/modules/roles/role.controller.ts
Normal file
94
identity-gateway/src/modules/roles/role.controller.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { FastifyInstance } from "fastify";
|
||||||
|
import { RoleService } from "./role.service";
|
||||||
|
import { authGuard } from "../../core/auth.guard";
|
||||||
|
import { TokenService } from "../../core/token.service";
|
||||||
|
|
||||||
|
export const registerRoleRoutes = (
|
||||||
|
app: FastifyInstance,
|
||||||
|
roleService: RoleService,
|
||||||
|
tokenService: TokenService
|
||||||
|
) => {
|
||||||
|
// List all roles
|
||||||
|
app.get(
|
||||||
|
"/roles",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async () => {
|
||||||
|
return roleService.listRoles();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get role by ID
|
||||||
|
app.get(
|
||||||
|
"/roles/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
return roleService.findById(id);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create role
|
||||||
|
app.post(
|
||||||
|
"/roles",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { name, description } = request.body as { name: string; description?: string };
|
||||||
|
if (!name) {
|
||||||
|
reply.code(400).send({ message: "Name is required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const role = await roleService.createRole(name, description);
|
||||||
|
reply.code(201).send(role);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update role
|
||||||
|
app.put(
|
||||||
|
"/roles/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const { name, description } = request.body as { name: string; description?: string };
|
||||||
|
return roleService.updateRole(id, name, description);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete role
|
||||||
|
app.delete(
|
||||||
|
"/roles/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
await roleService.deleteRole(id);
|
||||||
|
reply.code(204).send();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assign role to user
|
||||||
|
app.post(
|
||||||
|
"/roles/:roleId/users/:userId",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { roleId, userId } = request.params as { roleId: string; userId: string };
|
||||||
|
const { tenantId } = request.body as { tenantId: string };
|
||||||
|
if (!tenantId) {
|
||||||
|
reply.code(400).send({ message: "tenantId is required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await roleService.assignRoleToUser(userId, tenantId, roleId);
|
||||||
|
reply.code(201).send({ success: true });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove role from user
|
||||||
|
app.delete(
|
||||||
|
"/roles/:roleId/users/:userId",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { roleId, userId } = request.params as { roleId: string; userId: string };
|
||||||
|
const { tenantId } = request.body as { tenantId: string };
|
||||||
|
await roleService.removeRoleFromUser(userId, tenantId, roleId);
|
||||||
|
reply.code(204).send();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,19 +1,75 @@
|
||||||
import { db } from "../../lib/db";
|
import { db } from "../../lib/db";
|
||||||
import { RoleEntity } from "./role.entity";
|
|
||||||
|
export interface RoleEntity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export class RoleService {
|
export class RoleService {
|
||||||
async createRole(name: string, description?: string) {
|
async createRole(name: string, description?: string): Promise<RoleEntity> {
|
||||||
const result = await db.query<RoleEntity>(
|
const result = await db.query<RoleEntity>(
|
||||||
"INSERT INTO roles (name, description) VALUES ($1, $2) RETURNING id, name, description",
|
"INSERT INTO roles (name, description) VALUES ($1, $2) RETURNING id, name, description",
|
||||||
[name, description ?? null]
|
[name, description || null]
|
||||||
);
|
);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async listRoles() {
|
async findById(id: string): Promise<RoleEntity> {
|
||||||
|
const result = await db.query<RoleEntity>(
|
||||||
|
"SELECT id, name, description FROM roles WHERE id = $1",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
throw new Error("Role not found");
|
||||||
|
}
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByName(name: string): Promise<RoleEntity | null> {
|
||||||
|
const result = await db.query<RoleEntity>(
|
||||||
|
"SELECT id, name, description FROM roles WHERE name = $1",
|
||||||
|
[name]
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRoles(): Promise<RoleEntity[]> {
|
||||||
const result = await db.query<RoleEntity>(
|
const result = await db.query<RoleEntity>(
|
||||||
"SELECT id, name, description FROM roles ORDER BY name"
|
"SELECT id, name, description FROM roles ORDER BY name"
|
||||||
);
|
);
|
||||||
return result.rows;
|
return result.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateRole(id: string, name: string, description?: string): Promise<RoleEntity> {
|
||||||
|
const result = await db.query<RoleEntity>(
|
||||||
|
"UPDATE roles SET name = $2, description = $3 WHERE id = $1 RETURNING id, name, description",
|
||||||
|
[id, name, description || null]
|
||||||
|
);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
throw new Error("Role not found");
|
||||||
|
}
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRole(id: string): Promise<void> {
|
||||||
|
const result = await db.query("DELETE FROM roles WHERE id = $1", [id]);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
throw new Error("Role not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignRoleToUser(userId: string, tenantId: string, roleId: string): Promise<void> {
|
||||||
|
await db.query(
|
||||||
|
"INSERT INTO user_roles (user_id, tenant_id, role_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
|
||||||
|
[userId, tenantId, roleId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeRoleFromUser(userId: string, tenantId: string, roleId: string): Promise<void> {
|
||||||
|
await db.query(
|
||||||
|
"DELETE FROM user_roles WHERE user_id = $1 AND tenant_id = $2 AND role_id = $3",
|
||||||
|
[userId, tenantId, roleId]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
identity-gateway/src/modules/tenants/tenant.controller.ts
Normal file
66
identity-gateway/src/modules/tenants/tenant.controller.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { FastifyInstance } from "fastify";
|
||||||
|
import { TenantService } from "./tenant.service";
|
||||||
|
import { authGuard } from "../../core/auth.guard";
|
||||||
|
import { TokenService } from "../../core/token.service";
|
||||||
|
|
||||||
|
export const registerTenantRoutes = (
|
||||||
|
app: FastifyInstance,
|
||||||
|
tenantService: TenantService,
|
||||||
|
tokenService: TokenService
|
||||||
|
) => {
|
||||||
|
// List all tenants
|
||||||
|
app.get(
|
||||||
|
"/tenants",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async () => {
|
||||||
|
return tenantService.listTenants();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get tenant by ID
|
||||||
|
app.get(
|
||||||
|
"/tenants/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
return tenantService.findById(id);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create tenant
|
||||||
|
app.post(
|
||||||
|
"/tenants",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { name } = request.body as { name: string };
|
||||||
|
if (!name) {
|
||||||
|
reply.code(400).send({ message: "Name is required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tenant = await tenantService.createTenant(name);
|
||||||
|
reply.code(201).send(tenant);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update tenant
|
||||||
|
app.put(
|
||||||
|
"/tenants/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const { name } = request.body as { name: string };
|
||||||
|
return tenantService.updateTenant(id, name);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete tenant
|
||||||
|
app.delete(
|
||||||
|
"/tenants/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
await tenantService.deleteTenant(id);
|
||||||
|
reply.code(204).send();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
61
identity-gateway/src/modules/tenants/tenant.service.ts
Normal file
61
identity-gateway/src/modules/tenants/tenant.service.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { db } from "../../lib/db";
|
||||||
|
|
||||||
|
export interface TenantEntity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TenantService {
|
||||||
|
async createTenant(name: string): Promise<TenantEntity> {
|
||||||
|
const result = await db.query<TenantEntity>(
|
||||||
|
'INSERT INTO tenants (name) VALUES ($1) RETURNING id, name, created_at as "createdAt"',
|
||||||
|
[name]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<TenantEntity> {
|
||||||
|
const result = await db.query<TenantEntity>(
|
||||||
|
'SELECT id, name, created_at as "createdAt" FROM tenants WHERE id = $1',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
throw new Error("Tenant not found");
|
||||||
|
}
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByName(name: string): Promise<TenantEntity | null> {
|
||||||
|
const result = await db.query<TenantEntity>(
|
||||||
|
'SELECT id, name, created_at as "createdAt" FROM tenants WHERE name = $1',
|
||||||
|
[name]
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTenants(): Promise<TenantEntity[]> {
|
||||||
|
const result = await db.query<TenantEntity>(
|
||||||
|
'SELECT id, name, created_at as "createdAt" FROM tenants ORDER BY created_at DESC'
|
||||||
|
);
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTenant(id: string, name: string): Promise<TenantEntity> {
|
||||||
|
const result = await db.query<TenantEntity>(
|
||||||
|
'UPDATE tenants SET name = $2 WHERE id = $1 RETURNING id, name, created_at as "createdAt"',
|
||||||
|
[id, name]
|
||||||
|
);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
throw new Error("Tenant not found");
|
||||||
|
}
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTenant(id: string): Promise<void> {
|
||||||
|
const result = await db.query("DELETE FROM tenants WHERE id = $1", [id]);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
throw new Error("Tenant not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ export const registerUserRoutes = (
|
||||||
userService: UserService,
|
userService: UserService,
|
||||||
tokenService: TokenService
|
tokenService: TokenService
|
||||||
) => {
|
) => {
|
||||||
|
// Get current user
|
||||||
app.get(
|
app.get(
|
||||||
"/users/me",
|
"/users/me",
|
||||||
{ preHandler: authGuard(tokenService) },
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
|
@ -22,4 +23,103 @@ export const registerUserRoutes = (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// List all users
|
||||||
|
app.get(
|
||||||
|
"/users",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async () => {
|
||||||
|
return userService.listUsers();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get user by ID
|
||||||
|
app.get(
|
||||||
|
"/users/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const user = await userService.findById(id);
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
identifier: user.identifier,
|
||||||
|
status: user.status,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
app.post(
|
||||||
|
"/users",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { identifier, password, tenantId } = request.body as {
|
||||||
|
identifier: string;
|
||||||
|
password: string;
|
||||||
|
tenantId?: string;
|
||||||
|
};
|
||||||
|
if (!identifier || !password) {
|
||||||
|
reply.code(400).send({ message: "identifier and password are required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user = await userService.createUser(identifier, password);
|
||||||
|
if (tenantId) {
|
||||||
|
await userService.addUserToTenant(user.id, tenantId);
|
||||||
|
}
|
||||||
|
reply.code(201).send({
|
||||||
|
id: user.id,
|
||||||
|
identifier: user.identifier,
|
||||||
|
status: user.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update user
|
||||||
|
app.put(
|
||||||
|
"/users/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
const { identifier, status, password } = request.body as {
|
||||||
|
identifier?: string;
|
||||||
|
status?: string;
|
||||||
|
password?: string;
|
||||||
|
};
|
||||||
|
return userService.updateUser(id, { identifier, status, password });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete user
|
||||||
|
app.delete(
|
||||||
|
"/users/:id",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { id } = request.params as { id: string };
|
||||||
|
await userService.deleteUser(id);
|
||||||
|
reply.code(204).send();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add user to tenant
|
||||||
|
app.post(
|
||||||
|
"/users/:userId/tenants/:tenantId",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { userId, tenantId } = request.params as { userId: string; tenantId: string };
|
||||||
|
await userService.addUserToTenant(userId, tenantId);
|
||||||
|
reply.code(201).send({ success: true });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove user from tenant
|
||||||
|
app.delete(
|
||||||
|
"/users/:userId/tenants/:tenantId",
|
||||||
|
{ preHandler: authGuard(tokenService) },
|
||||||
|
async (request, reply) => {
|
||||||
|
const { userId, tenantId } = request.params as { userId: string; tenantId: string };
|
||||||
|
await userService.removeUserFromTenant(userId, tenantId);
|
||||||
|
reply.code(204).send();
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,51 @@ export class UserService {
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listUsers() {
|
||||||
|
const result = await db.query<UserEntity>(
|
||||||
|
"SELECT id, identifier, status, created_at as \"createdAt\" FROM users ORDER BY created_at DESC"
|
||||||
|
);
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser(userId: string, updates: { identifier?: string; status?: string; password?: string }) {
|
||||||
|
const user = await this.findById(userId);
|
||||||
|
const newIdentifier = updates.identifier ?? user.identifier;
|
||||||
|
const newStatus = updates.status ?? user.status;
|
||||||
|
let newPasswordHash = user.passwordHash;
|
||||||
|
|
||||||
|
if (updates.password) {
|
||||||
|
newPasswordHash = await hashSecret(updates.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.query<UserEntity>(
|
||||||
|
"UPDATE users SET identifier = $2, status = $3, password_hash = $4 WHERE id = $1 RETURNING id, identifier, status, created_at as \"createdAt\"",
|
||||||
|
[userId, newIdentifier, newStatus, newPasswordHash]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(userId: string) {
|
||||||
|
const result = await db.query("DELETE FROM users WHERE id = $1", [userId]);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addUserToTenant(userId: string, tenantId: string) {
|
||||||
|
await db.query(
|
||||||
|
"INSERT INTO user_tenants (user_id, tenant_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||||
|
[userId, tenantId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeUserFromTenant(userId: string, tenantId: string) {
|
||||||
|
await db.query(
|
||||||
|
"DELETE FROM user_tenants WHERE user_id = $1 AND tenant_id = $2",
|
||||||
|
[userId, tenantId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async verifyCredentials(identifier: string, password: string) {
|
async verifyCredentials(identifier: string, password: string) {
|
||||||
const user = await this.findByIdentifier(identifier);
|
const user = await this.findByIdentifier(identifier);
|
||||||
if (user.status !== "active") {
|
if (user.status !== "active") {
|
||||||
|
|
|
||||||
17
quick_push.sh
Executable file
17
quick_push.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
REGISTRY="rg.fr-par.scw.cloud/yumi"
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
echo "🚀 Building dashboard..."
|
||||||
|
podman build -t "$REGISTRY/dashboard:latest" ./dashboard
|
||||||
|
echo "🚀 Pushing dashboard..."
|
||||||
|
podman push "$REGISTRY/dashboard:latest"
|
||||||
|
|
||||||
|
# Identity Gateway
|
||||||
|
echo "🚀 Building identity-gateway..."
|
||||||
|
podman build -t "$REGISTRY/identity-gateway:latest" ./identity-gateway
|
||||||
|
echo "🚀 Pushing identity-gateway..."
|
||||||
|
podman push "$REGISTRY/identity-gateway:latest"
|
||||||
|
|
||||||
|
echo "✅ Quick push done!"
|
||||||
Loading…
Reference in a new issue