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 . .
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
if (!appwriteDatabaseId || !appwriteCollectionAuditLogsId) return undefined
|
||||
if (!appwriteDatabaseId || !appwriteCollectionAuditLogsId || !client) return undefined
|
||||
|
||||
const channel = `databases.${appwriteDatabaseId}.collections.${appwriteCollectionAuditLogsId}.documents`
|
||||
const unsubscribe = client.subscribe(channel, (response) => {
|
||||
|
|
|
|||
|
|
@ -31,22 +31,16 @@ export default function UserDropdown() {
|
|||
navigate('/login')
|
||||
}
|
||||
|
||||
const getInitials = (name?: string, email?: string) => {
|
||||
if (name) {
|
||||
const names = name.split(' ')
|
||||
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()
|
||||
const getInitials = (identifier?: string) => {
|
||||
if (identifier) {
|
||||
return identifier.substring(0, 2).toUpperCase()
|
||||
}
|
||||
return 'U'
|
||||
}
|
||||
|
||||
const initials = getInitials(user?.name, user?.email)
|
||||
const displayName = user?.name || user?.email?.split('@')[0] || 'Usuário'
|
||||
const displayEmail = user?.email || ''
|
||||
const initials = getInitials(user?.identifier)
|
||||
const displayName = user?.identifier?.split('@')[0] || 'Usuário'
|
||||
const displayEmail = user?.identifier || ''
|
||||
|
||||
return (
|
||||
<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 { Navigate, Outlet, useLocation } from 'react-router-dom'
|
||||
import { account } from '../lib/appwrite'
|
||||
import { getCurrentUser, loginUser, logoutUser, User } from '../lib/auth'
|
||||
|
||||
type AuthContextValue = {
|
||||
user: Models.User<Models.Preferences> | null
|
||||
user: User | null
|
||||
loading: boolean
|
||||
login: (email: string, password: string) => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
|
|
@ -14,21 +13,17 @@ type AuthContextValue = {
|
|||
const AuthContext = createContext<AuthContextValue | undefined>(undefined)
|
||||
|
||||
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 fetchUser = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const current = await account.get()
|
||||
const current = await getCurrentUser()
|
||||
setUser(current)
|
||||
} 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 {
|
||||
setLoading(false)
|
||||
}
|
||||
|
|
@ -40,14 +35,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
|
||||
const login = useCallback(
|
||||
async (email: string, password: string) => {
|
||||
await account.createEmailPasswordSession(email, password)
|
||||
await loginUser(email, password)
|
||||
await fetchUser()
|
||||
},
|
||||
[fetchUser],
|
||||
)
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await account.deleteSession('current')
|
||||
await logoutUser()
|
||||
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">
|
||||
<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" />
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
import { Account, AppwriteException, Client, Databases, Functions, Models } from 'appwrite'
|
||||
|
||||
const appwriteEndpoint = import.meta.env.VITE_APPWRITE_ENDPOINT
|
||||
const appwriteProjectId = import.meta.env.VITE_APPWRITE_PROJECT_ID
|
||||
const appwriteEndpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || ''
|
||||
const appwriteProjectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || ''
|
||||
const appwriteDatabaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID
|
||||
const appwriteCollectionServersId = import.meta.env.VITE_APPWRITE_COLLECTION_SERVERS_ID
|
||||
const appwriteCollectionGithubReposId = import.meta.env.VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID
|
||||
const appwriteCollectionAuditLogsId = import.meta.env.VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID
|
||||
const appwriteCollectionCloudflareAccountsId = import.meta.env.VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID
|
||||
|
||||
if (!appwriteEndpoint || !appwriteProjectId) {
|
||||
throw new Error('Defina VITE_APPWRITE_ENDPOINT e VITE_APPWRITE_PROJECT_ID no ambiente do Vite.')
|
||||
}
|
||||
// Appwrite is now optional - Identity Gateway is the primary auth provider
|
||||
const client = appwriteEndpoint && appwriteProjectId
|
||||
? new Client().setEndpoint(appwriteEndpoint).setProject(appwriteProjectId)
|
||||
: null
|
||||
|
||||
const client = new Client().setEndpoint(appwriteEndpoint).setProject(appwriteProjectId)
|
||||
|
||||
const account = new Account(client)
|
||||
const databases = new Databases(client)
|
||||
const functions = new Functions(client)
|
||||
const account = client ? new Account(client) : null
|
||||
const databases = client ? new Databases(client) : null
|
||||
const functions = client ? new Functions(client) : null
|
||||
|
||||
const isSessionExpired = (error: unknown) => {
|
||||
if (error instanceof AppwriteException) {
|
||||
|
|
@ -28,10 +27,12 @@ const isSessionExpired = (error: unknown) => {
|
|||
}
|
||||
|
||||
export const loginUser = async (email: string, password: string) => {
|
||||
if (!account) throw new Error('Appwrite not configured')
|
||||
return account.createEmailPasswordSession(email, password)
|
||||
}
|
||||
|
||||
export const logoutUser = async () => {
|
||||
if (!account) return
|
||||
try {
|
||||
await account.deleteSession('current')
|
||||
} catch (error) {
|
||||
|
|
@ -42,6 +43,7 @@ export const logoutUser = async () => {
|
|||
}
|
||||
|
||||
export const getCurrentUser = async (): Promise<Models.User<Models.Preferences> | null> => {
|
||||
if (!account) return null
|
||||
try {
|
||||
return await account.get()
|
||||
} 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>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
// Force rebuild Tue Dec 30 18:43:49 -03 2025
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ export default function AccountsAdmin() {
|
|||
|
||||
const fetchAccounts = async () => {
|
||||
try {
|
||||
if (!appwriteDatabaseId) return
|
||||
if (!appwriteDatabaseId || !databases) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
const response = await databases.listDocuments<Account>(
|
||||
appwriteDatabaseId,
|
||||
COLLECTION_ID,
|
||||
|
|
@ -157,8 +160,8 @@ export default function AccountsAdmin() {
|
|||
|
||||
{/* Status */}
|
||||
<span className={`rounded px-2 py-0.5 text-xs ${account.active
|
||||
? 'bg-emerald-500/20 text-emerald-300'
|
||||
: 'bg-slate-700/50 text-slate-400'
|
||||
? 'bg-emerald-500/20 text-emerald-300'
|
||||
: 'bg-slate-700/50 text-slate-400'
|
||||
}`}>
|
||||
{account.active ? 'Ativo' : 'Inativo'}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default function Cloudflare() {
|
|||
|
||||
useEffect(() => {
|
||||
const loadCredentials = async () => {
|
||||
if (!appwriteDatabaseId || !appwriteCollectionCloudflareAccountsId) return
|
||||
if (!appwriteDatabaseId || !appwriteCollectionCloudflareAccountsId || !databases) return
|
||||
try {
|
||||
const response = await databases.listDocuments(appwriteDatabaseId, appwriteCollectionCloudflareAccountsId, [
|
||||
Query.equal('provider', 'cloudflare'),
|
||||
|
|
@ -54,7 +54,7 @@ export default function Cloudflare() {
|
|||
}
|
||||
|
||||
const loadWorkers = async () => {
|
||||
if (!appwriteDatabaseId || !appwriteCollectionServersId) return
|
||||
if (!appwriteDatabaseId || !appwriteCollectionServersId || !databases) return
|
||||
try {
|
||||
const servers = await databases.listDocuments<Models.Document & { status?: string }>(
|
||||
appwriteDatabaseId,
|
||||
|
|
@ -76,7 +76,7 @@ export default function Cloudflare() {
|
|||
const handleSaveKey = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
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.')
|
||||
return
|
||||
}
|
||||
|
|
@ -103,6 +103,10 @@ export default function Cloudflare() {
|
|||
setError('Selecione ou cadastre uma credencial Cloudflare antes de consultar o status.')
|
||||
return
|
||||
}
|
||||
if (!functions) {
|
||||
setError('Appwrite functions not configured.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const executionResult = await functions.createExecution(
|
||||
'check-cloudflare-status',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ export default function Github() {
|
|||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
if (!functions) {
|
||||
setError('Appwrite functions not configured.')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const executionResult = await functions.createExecution(
|
||||
'sync-github',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ export default function Hello() {
|
|||
setError(null)
|
||||
setMessage(null)
|
||||
|
||||
if (!functions) {
|
||||
setError('Appwrite functions not configured.')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const executionResult = await functions.createExecution(
|
||||
'hello-world',
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ export default function Home() {
|
|||
|
||||
const fetchMetrics = async () => {
|
||||
try {
|
||||
if (!databases) {
|
||||
setError('Appwrite not configured. Configure VITE_APPWRITE_* variables to enable metrics.')
|
||||
return
|
||||
}
|
||||
const [repos, servers, deployments] = await Promise.all([
|
||||
databases.listDocuments<Repo>(appwriteDatabaseId!, appwriteCollectionGithubReposId!, [Query.limit(1)]),
|
||||
databases.listDocuments<Server>(appwriteDatabaseId!, appwriteCollectionServersId!, [Query.limit(200)]),
|
||||
|
|
|
|||
|
|
@ -4,23 +4,22 @@ import { useAuth } from '../contexts/Auth'
|
|||
export default function Profile() {
|
||||
const { user } = useAuth()
|
||||
|
||||
const getInitials = (name?: string, email?: string) => {
|
||||
if (name) {
|
||||
const names = name.split(' ')
|
||||
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()
|
||||
const getInitials = (identifier?: string) => {
|
||||
if (identifier) {
|
||||
// If it's an email, use first 2 chars before @
|
||||
const atIndex = identifier.indexOf('@')
|
||||
if (atIndex > 0) {
|
||||
return identifier.substring(0, 2).toUpperCase()
|
||||
}
|
||||
return identifier.substring(0, 2).toUpperCase()
|
||||
}
|
||||
return 'U'
|
||||
}
|
||||
|
||||
const initials = getInitials(user?.name, user?.email)
|
||||
const displayName = user?.name || 'Usuário'
|
||||
const displayEmail = user?.email || 'email@exemplo.com'
|
||||
const createdAt = user?.$createdAt ? new Date(user.$createdAt).toLocaleDateString('pt-BR') : 'N/A'
|
||||
const initials = getInitials(user?.identifier)
|
||||
const displayName = user?.identifier?.split('@')[0] || 'Usuário'
|
||||
const displayEmail = user?.identifier || 'email@exemplo.com'
|
||||
const createdAt = 'N/A' // Identity Gateway doesn't return createdAt in /users/me
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -72,7 +71,7 @@ export default function Profile() {
|
|||
<User size={18} className="text-cyan-400" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<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>
|
||||
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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",
|
||||
"dotenv": "^16.4.5",
|
||||
"fastify": "^4.28.1",
|
||||
"@fastify/cookie": "^9.3.1",
|
||||
"fastify": "^5.6.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.12.0",
|
||||
"pino": "^8.19.0"
|
||||
"pino": "^8.19.0",
|
||||
"pino-pretty": "^11.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/pg": "^8.11.6",
|
||||
"eslint": "^9.6.0",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface TokenPair {
|
|||
export class TokenService {
|
||||
signAccessToken(payload: TenantContext) {
|
||||
return jwt.sign(payload, env.jwtAccessSecret, {
|
||||
expiresIn: env.jwtAccessTtl,
|
||||
expiresIn: env.jwtAccessTtl as any,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ export class TokenService {
|
|||
},
|
||||
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 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 { logger } from "./lib/logger";
|
||||
import { seed } from "./lib/seed";
|
||||
import { TokenService } from "./core/token.service";
|
||||
import { UserService } from "./modules/users/user.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 { registerAuthRoutes } from "./modules/auth/auth.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 () => {
|
||||
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);
|
||||
|
||||
// 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 userService = new UserService();
|
||||
const sessionService = new SessionService();
|
||||
const tenantService = new TenantService();
|
||||
const roleService = new RoleService();
|
||||
|
||||
const providers = new Map<string, AuthProvider>();
|
||||
providers.set("local", new LocalProvider(userService));
|
||||
|
|
@ -31,8 +119,13 @@ const bootstrap = async () => {
|
|||
providers
|
||||
);
|
||||
|
||||
registerAuthRoutes(app, authService);
|
||||
registerUserRoutes(app, userService, tokenService);
|
||||
registerAuthRoutes(app as any, authService);
|
||||
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" });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,20 @@ import { AuthService } from "./auth.service";
|
|||
import { env } from "../../lib/env";
|
||||
|
||||
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 {
|
||||
provider?: 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 { RoleEntity } from "./role.entity";
|
||||
|
||||
export interface RoleEntity {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export class RoleService {
|
||||
async createRole(name: string, description?: string) {
|
||||
async createRole(name: string, description?: string): Promise<RoleEntity> {
|
||||
const result = await db.query<RoleEntity>(
|
||||
"INSERT INTO roles (name, description) VALUES ($1, $2) RETURNING id, name, description",
|
||||
[name, description ?? null]
|
||||
[name, description || null]
|
||||
);
|
||||
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>(
|
||||
"SELECT id, name, description FROM roles ORDER BY name"
|
||||
);
|
||||
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,
|
||||
tokenService: TokenService
|
||||
) => {
|
||||
// Get current user
|
||||
app.get(
|
||||
"/users/me",
|
||||
{ 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];
|
||||
}
|
||||
|
||||
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) {
|
||||
const user = await this.findByIdentifier(identifier);
|
||||
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