You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 18:05:34 +03:00 
			
		
		
		
	API lib cleanup, 404 hosts WIP
This commit is contained in:
		| @@ -1,13 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AccessList } from "./models"; | ||||
|  | ||||
| export async function createAccessList(item: AccessList, abortController?: AbortController): Promise<AccessList> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function createAccessList(item: AccessList): Promise<AccessList> { | ||||
| 	return await api.post({ | ||||
| 		url: "/nginx/access-lists", | ||||
| 		// todo: only use whitelist of fields for this data | ||||
| 		data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function createCertificate(item: Certificate, abortController?: AbortController): Promise<Certificate> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function createCertificate(item: Certificate): Promise<Certificate> { | ||||
| 	return await api.post({ | ||||
| 		url: "/nginx/certificates", | ||||
| 		// todo: only use whitelist of fields for this data | ||||
| 		data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { DeadHost } from "./models"; | ||||
|  | ||||
| export async function createDeadHost(item: DeadHost, abortController?: AbortController): Promise<DeadHost> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function createDeadHost(item: DeadHost): Promise<DeadHost> { | ||||
| 	return await api.post({ | ||||
| 		url: "/nginx/dead-hosts", | ||||
| 		// todo: only use whitelist of fields for this data | ||||
| 		data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export async function createProxyHost(item: ProxyHost, abortController?: AbortController): Promise<ProxyHost> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function createProxyHost(item: ProxyHost): Promise<ProxyHost> { | ||||
| 	return await api.post({ | ||||
| 		url: "/nginx/proxy-hosts", | ||||
| 		// todo: only use whitelist of fields for this data | ||||
| 		data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,16 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { RedirectionHost } from "./models"; | ||||
|  | ||||
| export async function createRedirectionHost( | ||||
| 	item: RedirectionHost, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<RedirectionHost> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function createRedirectionHost(item: RedirectionHost): Promise<RedirectionHost> { | ||||
| 	return await api.post({ | ||||
| 		url: "/nginx/redirection-hosts", | ||||
| 		// todo: only use whitelist of fields for this data | ||||
| 		data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Stream } from "./models"; | ||||
|  | ||||
| export async function createStream(item: Stream, abortController?: AbortController): Promise<Stream> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function createStream(item: Stream): Promise<Stream> { | ||||
| 	return await api.post({ | ||||
| 		url: "/nginx/streams", | ||||
| 		// todo: only use whitelist of fields for this data | ||||
| 		data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -15,14 +15,11 @@ export interface NewUser { | ||||
| 	roles?: string[]; | ||||
| } | ||||
|  | ||||
| export async function createUser(item: NewUser, noAuth?: boolean, abortController?: AbortController): Promise<User> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function createUser(item: NewUser, noAuth?: boolean): Promise<User> { | ||||
| 	return await api.post({ | ||||
| 		url: "/users", | ||||
| 		// todo: only use whitelist of fields for this data | ||||
| 		data: item, | ||||
| 		noAuth, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteAccessList(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| export async function deleteAccessList(id: number): Promise<boolean> { | ||||
| 	return await api.del({ | ||||
| 		url: `/nginx/access-lists/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteCertificate(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| export async function deleteCertificate(id: number): Promise<boolean> { | ||||
| 	return await api.del({ | ||||
| 		url: `/nginx/certificates/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteDeadHost(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| export async function deleteDeadHost(id: number): Promise<boolean> { | ||||
| 	return await api.del({ | ||||
| 		url: `/nginx/dead-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteProxyHost(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| export async function deleteProxyHost(id: number): Promise<boolean> { | ||||
| 	return await api.del({ | ||||
| 		url: `/nginx/proxy-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteRedirectionHost(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| export async function deleteRedirectionHost(id: number): Promise<boolean> { | ||||
| 	return await api.del({ | ||||
| 		url: `/nginx/redirection-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteStream(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| export async function deleteStream(id: number): Promise<boolean> { | ||||
| 	return await api.del({ | ||||
| 		url: `/nginx/streams/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteUser(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| export async function deleteUser(id: number): Promise<boolean> { | ||||
| 	return await api.del({ | ||||
| 		url: `/users/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,8 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Binary } from "./responseTypes"; | ||||
|  | ||||
| export async function downloadCertificate(id: number, abortController?: AbortController): Promise<Binary> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function downloadCertificate(id: number): Promise<Binary> { | ||||
| 	return await api.get({ | ||||
| 		url: `/nginx/certificates/${id}/download`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
							
								
								
									
										6
									
								
								frontend/src/api/backend/expansions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/api/backend/expansions.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| export type AccessListExpansion = "owner" | "items" | "clients"; | ||||
| export type AuditLogExpansion = "user"; | ||||
| export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts"; | ||||
| export type HostExpansion = "owner" | "certificate"; | ||||
| export type ProxyHostExpansion = "owner" | "access_list" | "certificate"; | ||||
| export type UserExpansion = "permissions"; | ||||
| @@ -1,11 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AccessListExpansion } from "./expansions"; | ||||
| import type { AccessList } from "./models"; | ||||
|  | ||||
| export async function getAccessList(id: number, abortController?: AbortController): Promise<AccessList> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getAccessList(id: number, expand?: AccessListExpansion[], params = {}): Promise<AccessList> { | ||||
| 	return await api.get({ | ||||
| 		url: `/nginx/access-lists/${id}`, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AccessListExpansion } from "./expansions"; | ||||
| import type { AccessList } from "./models"; | ||||
|  | ||||
| export type AccessListExpansion = "owner" | "items" | "clients"; | ||||
|  | ||||
| export async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise<AccessList[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/access-lists", | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AuditLogExpansion } from "./getAuditLogs"; | ||||
| import type { AuditLogExpansion } from "./expansions"; | ||||
| import type { AuditLog } from "./models"; | ||||
|  | ||||
| export async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise<AuditLog> { | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AuditLogExpansion } from "./expansions"; | ||||
| import type { AuditLog } from "./models"; | ||||
|  | ||||
| export type AuditLogExpansion = "user"; | ||||
|  | ||||
| export async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise<AuditLog[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/audit-log", | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { CertificateExpansion } from "./expansions"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function getCertificate(id: number, abortController?: AbortController): Promise<Certificate> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getCertificate(id: number, expand?: CertificateExpansion[], params = {}): Promise<Certificate> { | ||||
| 	return await api.get({ | ||||
| 		url: `/nginx/certificates/${id}`, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
| import type { CertificateExpansion } from "./expansions"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts"; | ||||
|  | ||||
| export async function getCertificates(expand?: CertificateExpansion[], params = {}): Promise<Certificate[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/certificates", | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { HostExpansion } from "./expansions"; | ||||
| import type { DeadHost } from "./models"; | ||||
|  | ||||
| export async function getDeadHost(id: number, abortController?: AbortController): Promise<DeadHost> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getDeadHost(id: number, expand?: HostExpansion[], params = {}): Promise<DeadHost> { | ||||
| 	return await api.get({ | ||||
| 		url: `/nginx/dead-hosts/${id}`, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import * as api from "./base"; | ||||
| import type { HostExpansion } from "./expansions"; | ||||
| import type { DeadHost } from "./models"; | ||||
|  | ||||
| export type DeadHostExpansion = "owner" | "certificate"; | ||||
|  | ||||
| export async function getDeadHosts(expand?: DeadHostExpansion[], params = {}): Promise<DeadHost[]> { | ||||
| export async function getDeadHosts(expand?: HostExpansion[], params = {}): Promise<DeadHost[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/dead-hosts", | ||||
| 		params: { | ||||
|   | ||||
| @@ -1,11 +1,8 @@ | ||||
| import * as api from "./base"; | ||||
| import type { HealthResponse } from "./responseTypes"; | ||||
|  | ||||
| export async function getHealth(abortController?: AbortController): Promise<HealthResponse> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getHealth(): Promise<HealthResponse> { | ||||
| 	return await api.get({ | ||||
| 		url: "/", | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function getHostsReport(abortController?: AbortController): Promise<Record<string, number>> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getHostsReport(): Promise<Record<string, number>> { | ||||
| 	return await api.get({ | ||||
| 		url: "/reports/hosts", | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHostExpansion } from "./expansions"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export async function getProxyHost(id: number, abortController?: AbortController): Promise<ProxyHost> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getProxyHost(id: number, expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost> { | ||||
| 	return await api.get({ | ||||
| 		url: `/nginx/proxy-hosts/${id}`, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHostExpansion } from "./expansions"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export type ProxyHostExpansion = "owner" | "access_list" | "certificate"; | ||||
|  | ||||
| export async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/proxy-hosts", | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { HostExpansion } from "./expansions"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export async function getRedirectionHost(id: number, abortController?: AbortController): Promise<ProxyHost> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise<ProxyHost> { | ||||
| 	return await api.get({ | ||||
| 		url: `/nginx/redirection-hosts/${id}`, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,8 @@ | ||||
| import * as api from "./base"; | ||||
| import type { HostExpansion } from "./expansions"; | ||||
| import type { RedirectionHost } from "./models"; | ||||
|  | ||||
| export type RedirectionHostExpansion = "owner" | "certificate"; | ||||
| export async function getRedirectionHosts( | ||||
| 	expand?: RedirectionHostExpansion[], | ||||
| 	params = {}, | ||||
| ): Promise<RedirectionHost[]> { | ||||
| export async function getRedirectionHosts(expand?: HostExpansion[], params = {}): Promise<RedirectionHost[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/redirection-hosts", | ||||
| 		params: { | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Setting } from "./models"; | ||||
|  | ||||
| export async function getSetting(id: string, abortController?: AbortController): Promise<Setting> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getSetting(id: string, expand?: string[], params = {}): Promise<Setting> { | ||||
| 	return await api.get({ | ||||
| 		url: `/settings/${id}`, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { HostExpansion } from "./expansions"; | ||||
| import type { Stream } from "./models"; | ||||
|  | ||||
| export async function getStream(id: number, abortController?: AbortController): Promise<Stream> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function getStream(id: number, expand?: HostExpansion[], params = {}): Promise<Stream> { | ||||
| 	return await api.get({ | ||||
| 		url: `/nginx/streams/${id}`, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import * as api from "./base"; | ||||
| import type { HostExpansion } from "./expansions"; | ||||
| import type { Stream } from "./models"; | ||||
|  | ||||
| export type StreamExpansion = "owner" | "certificate"; | ||||
|  | ||||
| export async function getStreams(expand?: StreamExpansion[], params = {}): Promise<Stream[]> { | ||||
| export async function getStreams(expand?: HostExpansion[], params = {}): Promise<Stream[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/streams", | ||||
| 		params: { | ||||
|   | ||||
| @@ -1,19 +1,9 @@ | ||||
| import * as api from "./base"; | ||||
| import type { TokenResponse } from "./responseTypes"; | ||||
|  | ||||
| interface Options { | ||||
| 	payload: { | ||||
| 		identity: string; | ||||
| 		secret: string; | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| export async function getToken({ payload }: Options, abortController?: AbortController): Promise<TokenResponse> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function getToken(identity: string, secret: string): Promise<TokenResponse> { | ||||
| 	return await api.post({ | ||||
| 		url: "/tokens", | ||||
| 			data: payload, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 		data: { identity, secret }, | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
| import type { UserExpansion } from "./expansions"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export async function getUser(id: number | string = "me", params = {}): Promise<User> { | ||||
| export async function getUser(id: number | string = "me", expand?: UserExpansion[], params = {}): Promise<User> { | ||||
| 	const userId = id ? id : "me"; | ||||
| 	return await api.get({ | ||||
| 		url: `/users/${userId}`, | ||||
| 		params, | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
| import type { UserExpansion } from "./expansions"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export type UserExpansion = "permissions"; | ||||
|  | ||||
| export async function getUsers(expand?: UserExpansion[], params = {}): Promise<User[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/users", | ||||
|   | ||||
| @@ -13,6 +13,7 @@ export * from "./deleteRedirectionHost"; | ||||
| export * from "./deleteStream"; | ||||
| export * from "./deleteUser"; | ||||
| export * from "./downloadCertificate"; | ||||
| export * from "./expansions"; | ||||
| export * from "./getAccessList"; | ||||
| export * from "./getAccessLists"; | ||||
| export * from "./getAuditLog"; | ||||
|   | ||||
| @@ -1,11 +1,8 @@ | ||||
| import * as api from "./base"; | ||||
| import type { TokenResponse } from "./responseTypes"; | ||||
|  | ||||
| export async function refreshToken(abortController?: AbortController): Promise<TokenResponse> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function refreshToken(): Promise<TokenResponse> { | ||||
| 	return await api.get({ | ||||
| 		url: "/tokens", | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,8 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function renewCertificate(id: number, abortController?: AbortController): Promise<Certificate> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function renewCertificate(id: number): Promise<Certificate> { | ||||
| 	return await api.post({ | ||||
| 		url: `/nginx/certificates/${id}/renew`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { UserPermissions } from "./models"; | ||||
|  | ||||
| export async function setPermissions( | ||||
| 	userId: number, | ||||
| 	data: UserPermissions, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<boolean> { | ||||
| export async function setPermissions(userId: number, data: UserPermissions): Promise<boolean> { | ||||
| 	// Remove readonly fields | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/users/${userId}/permissions`, | ||||
| 		data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,16 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function testHttpCertificate( | ||||
| 	domains: string[], | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<Record<string, string>> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| export async function testHttpCertificate(domains: string[]): Promise<Record<string, string>> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/certificates/test-http", | ||||
| 		params: { | ||||
| 			domains: domains.join(","), | ||||
| 		}, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function toggleDeadHost( | ||||
| 	id: number, | ||||
| 	enabled: boolean, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<boolean> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function toggleDeadHost(id: number, enabled: boolean): Promise<boolean> { | ||||
| 	return await api.post({ | ||||
| 		url: `/nginx/dead-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function toggleProxyHost( | ||||
| 	id: number, | ||||
| 	enabled: boolean, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<boolean> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function toggleProxyHost(id: number, enabled: boolean): Promise<boolean> { | ||||
| 	return await api.post({ | ||||
| 		url: `/nginx/proxy-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function toggleRedirectionHost( | ||||
| 	id: number, | ||||
| 	enabled: boolean, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<boolean> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function toggleRedirectionHost(id: number, enabled: boolean): Promise<boolean> { | ||||
| 	return await api.post({ | ||||
| 		url: `/nginx/redirection-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function toggleStream(id: number, enabled: boolean, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| export async function toggleStream(id: number, enabled: boolean): Promise<boolean> { | ||||
| 	return await api.post({ | ||||
| 		url: `/nginx/streams/${id}/${enabled ? "enable" : "disable"}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AccessList } from "./models"; | ||||
|  | ||||
| export async function updateAccessList(item: AccessList, abortController?: AbortController): Promise<AccessList> { | ||||
| export async function updateAccessList(item: AccessList): Promise<AccessList> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/nginx/access-lists/${id}`, | ||||
| 		data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,7 @@ | ||||
| import * as api from "./base"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export async function updateAuth( | ||||
| 	userId: number | "me", | ||||
| 	newPassword: string, | ||||
| 	current?: string, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<User> { | ||||
| export async function updateAuth(userId: number | "me", newPassword: string, current?: string): Promise<User> { | ||||
| 	const data = { | ||||
| 		type: "password", | ||||
| 		current: current, | ||||
| @@ -16,11 +11,8 @@ export async function updateAuth( | ||||
| 		data.current = current; | ||||
| 	} | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/users/${userId}/auth`, | ||||
| 		data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { DeadHost } from "./models"; | ||||
|  | ||||
| export async function updateDeadHost(item: DeadHost, abortController?: AbortController): Promise<DeadHost> { | ||||
| export async function updateDeadHost(item: DeadHost): Promise<DeadHost> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/nginx/dead-hosts/${id}`, | ||||
| 		data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export async function updateProxyHost(item: ProxyHost, abortController?: AbortController): Promise<ProxyHost> { | ||||
| export async function updateProxyHost(item: ProxyHost): Promise<ProxyHost> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/nginx/proxy-hosts/${id}`, | ||||
| 		data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,18 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { RedirectionHost } from "./models"; | ||||
|  | ||||
| export async function updateRedirectionHost( | ||||
| 	item: RedirectionHost, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<RedirectionHost> { | ||||
| export async function updateRedirectionHost(item: RedirectionHost): Promise<RedirectionHost> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/nginx/redirection-hosts/${id}`, | ||||
| 		data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Setting } from "./models"; | ||||
|  | ||||
| export async function updateSetting(item: Setting, abortController?: AbortController): Promise<Setting> { | ||||
| export async function updateSetting(item: Setting): Promise<Setting> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/settings/${id}`, | ||||
| 		data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Stream } from "./models"; | ||||
|  | ||||
| export async function updateStream(item: Stream, abortController?: AbortController): Promise<Stream> { | ||||
| export async function updateStream(item: Stream): Promise<Stream> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/nginx/streams/${id}`, | ||||
| 		data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export async function updateUser(item: User, abortController?: AbortController): Promise<User> { | ||||
| export async function updateUser(item: User): Promise<User> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 	return await api.put({ | ||||
| 		url: `/users/${id}`, | ||||
| 		data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -6,13 +6,9 @@ export async function uploadCertificate( | ||||
| 	certificate: string, | ||||
| 	certificateKey: string, | ||||
| 	intermediateCertificate?: string, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<Certificate> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 	return await api.post({ | ||||
| 		url: `/nginx/certificates/${id}/upload`, | ||||
| 		data: { certificate, certificateKey, intermediateCertificate }, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -5,13 +5,9 @@ export async function validateCertificate( | ||||
| 	certificate: string, | ||||
| 	certificateKey: string, | ||||
| 	intermediateCertificate?: string, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<ValidatedCertificateResponse> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 	return await api.post({ | ||||
| 		url: "/nginx/certificates/validate", | ||||
| 		data: { certificate, certificateKey, intermediateCertificate }, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { intl } from "src/locale"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { Button } from "src/components"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| export function ErrorNotFound() { | ||||
| 	const navigate = useNavigate(); | ||||
| @@ -9,9 +9,7 @@ export function ErrorNotFound() { | ||||
| 		<div className="container-tight py-4"> | ||||
| 			<div className="empty"> | ||||
| 				<p className="empty-title">{intl.formatMessage({ id: "notfound.title" })}</p> | ||||
| 				<p className="empty-subtitle text-secondary"> | ||||
| 					{intl.formatMessage({ id: "notfound.text" })} | ||||
| 				</p> | ||||
| 				<p className="empty-subtitle text-secondary">{intl.formatMessage({ id: "notfound.text" })}</p> | ||||
| 				<div className="empty-action"> | ||||
| 					<Button type="button" size="md" onClick={() => navigate("/")}> | ||||
| 						{intl.formatMessage({ id: "notfound.action" })} | ||||
|   | ||||
							
								
								
									
										119
									
								
								frontend/src/components/Form/DomainNamesField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								frontend/src/components/Form/DomainNamesField.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| import { Field, useFormikContext } from "formik"; | ||||
| import type { ActionMeta, MultiValue } from "react-select"; | ||||
| import CreatableSelect from "react-select/creatable"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| export type SelectOption = { | ||||
| 	label: string; | ||||
| 	value: string; | ||||
| 	color?: string; | ||||
| }; | ||||
|  | ||||
| interface Props { | ||||
| 	id?: string; | ||||
| 	maxDomains?: number; | ||||
| 	isWildcardPermitted?: boolean; | ||||
| 	dnsProviderWildcardSupported?: boolean; | ||||
| 	name?: string; | ||||
| 	label?: string; | ||||
| } | ||||
| export function DomainNamesField({ | ||||
| 	name = "domainNames", | ||||
| 	label = "domain-names", | ||||
| 	id = "domainNames", | ||||
| 	maxDomains, | ||||
| 	isWildcardPermitted, | ||||
| 	dnsProviderWildcardSupported, | ||||
| }: Props) { | ||||
| 	const { values, setFieldValue } = useFormikContext(); | ||||
|  | ||||
| 	const getDomainCount = (v: string[] | undefined): number => { | ||||
| 		if (v?.length) { | ||||
| 			return v.length; | ||||
| 		} | ||||
| 		return 0; | ||||
| 	}; | ||||
|  | ||||
| 	const handleChange = (v: MultiValue<SelectOption>, _actionMeta: ActionMeta<SelectOption>) => { | ||||
| 		const doms = v?.map((i: SelectOption) => { | ||||
| 			return i.value; | ||||
| 		}); | ||||
| 		setFieldValue(name, doms); | ||||
| 	}; | ||||
|  | ||||
| 	const isDomainValid = (d: string): boolean => { | ||||
| 		const dom = d.trim().toLowerCase(); | ||||
| 		const v: any = values; | ||||
|  | ||||
| 		// Deny if the list of domains is hit | ||||
| 		if (maxDomains && getDomainCount(v?.[name]) >= maxDomains) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		if (dom.length < 3) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		// Prevent wildcards | ||||
| 		if ((!isWildcardPermitted || !dnsProviderWildcardSupported) && dom.indexOf("*") !== -1) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		// Prevent duplicate * in domain | ||||
| 		if ((dom.match(/\*/g) || []).length > 1) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		// Prevent some invalid characters | ||||
| 		if ((dom.match(/(@|,|!|&|\$|#|%|\^|\(|\))/g) || []).length > 0) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		// This will match *.com type domains, | ||||
| 		return dom.match(/\*\.[^.]+$/m) === null; | ||||
| 	}; | ||||
|  | ||||
| 	const helperTexts: string[] = []; | ||||
| 	if (maxDomains) { | ||||
| 		helperTexts.push(intl.formatMessage({ id: "domain_names.max" }, { count: maxDomains })); | ||||
| 	} | ||||
| 	if (!isWildcardPermitted) { | ||||
| 		helperTexts.push(intl.formatMessage({ id: "wildcards-not-permitted" })); | ||||
| 	} else if (!dnsProviderWildcardSupported) { | ||||
| 		helperTexts.push(intl.formatMessage({ id: "wildcards-not-supported" })); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<Field name={name}> | ||||
| 			{({ field, form }: any) => ( | ||||
| 				<div className="mb-3"> | ||||
| 					<label className="form-label" htmlFor={id}> | ||||
| 						{intl.formatMessage({ id: label })} | ||||
| 					</label> | ||||
| 					<CreatableSelect | ||||
| 						name={field.name} | ||||
| 						id={id} | ||||
| 						closeMenuOnSelect={true} | ||||
| 						isClearable={false} | ||||
| 						isValidNewOption={isDomainValid} | ||||
| 						isMulti | ||||
| 						placeholder="Start typing to add domain..." | ||||
| 						onChange={handleChange} | ||||
| 						value={field.value?.map((d: string) => ({ label: d, value: d }))} | ||||
| 					/> | ||||
| 					{form.errors[field.name] ? ( | ||||
| 						<div className="invalid-feedback"> | ||||
| 							{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null} | ||||
| 						</div> | ||||
| 					) : helperTexts.length ? ( | ||||
| 						helperTexts.map((i) => ( | ||||
| 							<div key={i} className="invalid-feedback text-info"> | ||||
| 								{i} | ||||
| 							</div> | ||||
| 						)) | ||||
| 					) : null} | ||||
| 				</div> | ||||
| 			)} | ||||
| 		</Field> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										112
									
								
								frontend/src/components/Form/SSLCertificateField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								frontend/src/components/Form/SSLCertificateField.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| import { IconShield } from "@tabler/icons-react"; | ||||
| import { Field, useFormikContext } from "formik"; | ||||
| import Select, { type ActionMeta, components, type OptionProps } from "react-select"; | ||||
| import type { Certificate } from "src/api/backend"; | ||||
| import { useCertificates } from "src/hooks"; | ||||
| import { DateTimeFormat, intl } from "src/locale"; | ||||
|  | ||||
| interface CertOption { | ||||
| 	readonly value: number | "new"; | ||||
| 	readonly label: string; | ||||
| 	readonly subLabel: string; | ||||
| 	readonly icon: React.ReactNode; | ||||
| } | ||||
|  | ||||
| const Option = (props: OptionProps<CertOption>) => { | ||||
| 	return ( | ||||
| 		<components.Option {...props}> | ||||
| 			<div className="flex-fill"> | ||||
| 				<div className="font-weight-medium"> | ||||
| 					{props.data.icon} <strong>{props.data.label}</strong> | ||||
| 				</div> | ||||
| 				<div className="text-secondary mt-1 ps-3">{props.data.subLabel}</div> | ||||
| 			</div> | ||||
| 		</components.Option> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| interface Props { | ||||
| 	id?: string; | ||||
| 	name?: string; | ||||
| 	label?: string; | ||||
| 	required?: boolean; | ||||
| 	allowNew?: boolean; | ||||
| } | ||||
| export function SSLCertificateField({ | ||||
| 	name = "certificateId", | ||||
| 	label = "ssl-certificate", | ||||
| 	id = "certificateId", | ||||
| 	required, | ||||
| 	allowNew, | ||||
| }: Props) { | ||||
| 	const { isLoading, isError, error, data } = useCertificates(); | ||||
|  | ||||
| 	const { setFieldValue } = useFormikContext(); | ||||
|  | ||||
| 	const handleChange = (v: any, _actionMeta: ActionMeta<CertOption>) => { | ||||
| 		setFieldValue(name, v?.value); | ||||
| 	}; | ||||
|  | ||||
| 	const options: CertOption[] = | ||||
| 		data?.map((cert: Certificate) => ({ | ||||
| 			value: cert.id, | ||||
| 			label: cert.niceName, | ||||
| 			subLabel: `${cert.provider === "letsencrypt" ? "Let's Encrypt" : cert.provider} — Expires: ${ | ||||
| 				cert.expiresOn ? DateTimeFormat(cert.expiresOn) : "N/A" | ||||
| 			}`, | ||||
| 			icon: <IconShield size={14} className="text-pink" />, | ||||
| 		})) || []; | ||||
|  | ||||
| 	// Prepend the Add New option | ||||
| 	if (allowNew) { | ||||
| 		options?.unshift({ | ||||
| 			value: "new", | ||||
| 			label: "Request a new HTTP certificate", | ||||
| 			subLabel: "with Let's Encrypt", | ||||
| 			icon: <IconShield size={14} className="text-lime" />, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	// Prepend the None option | ||||
| 	if (!required) { | ||||
| 		options?.unshift({ | ||||
| 			value: 0, | ||||
| 			label: "None", | ||||
| 			subLabel: "This host will not use HTTPS", | ||||
| 			icon: <IconShield size={14} className="text-red" />, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<Field name={name}> | ||||
| 			{({ field, form }: any) => ( | ||||
| 				<div className="mb-3"> | ||||
| 					<label className="form-label" htmlFor={id}> | ||||
| 						{intl.formatMessage({ id: label })} | ||||
| 					</label> | ||||
| 					{isLoading ? <div className="placeholder placeholder-lg col-12 my-3 placeholder-glow" /> : null} | ||||
| 					{isError ? <div className="invalid-feedback">{`${error}`}</div> : null} | ||||
| 					{!isLoading && !isError ? ( | ||||
| 						<Select | ||||
| 							defaultValue={options[0]} | ||||
| 							options={options} | ||||
| 							components={{ Option }} | ||||
| 							styles={{ | ||||
| 								option: (base) => ({ | ||||
| 									...base, | ||||
| 									height: "100%", | ||||
| 								}), | ||||
| 							}} | ||||
| 							onChange={handleChange} | ||||
| 						/> | ||||
| 					) : null} | ||||
| 					{form.errors[field.name] ? ( | ||||
| 						<div className="invalid-feedback"> | ||||
| 							{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null} | ||||
| 						</div> | ||||
| 					) : null} | ||||
| 				</div> | ||||
| 			)} | ||||
| 		</Field> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										2
									
								
								frontend/src/components/Form/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								frontend/src/components/Form/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export * from "./DomainNamesField"; | ||||
| export * from "./SSLCertificateField"; | ||||
| @@ -7,11 +7,9 @@ function TableBody<T>(props: TableLayoutProps<T>) { | ||||
| 	const rows = tableInstance.getRowModel().rows; | ||||
|  | ||||
| 	if (rows.length === 0) { | ||||
| 		return emptyState ? ( | ||||
| 			emptyState | ||||
| 		) : ( | ||||
| 		return ( | ||||
| 			<tbody className="table-tbody"> | ||||
| 				<EmptyRow tableInstance={tableInstance} /> | ||||
| 				{emptyState ? emptyState : <EmptyRow tableInstance={tableInstance} />} | ||||
| 			</tbody> | ||||
| 		); | ||||
| 	} | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| export * from "./Button"; | ||||
| export * from "./ErrorNotFound"; | ||||
| export * from "./Flag"; | ||||
| export * from "./Form"; | ||||
| export * from "./HasPermission"; | ||||
| export * from "./Loading"; | ||||
| export * from "./LoadingPage"; | ||||
|   | ||||
| @@ -30,7 +30,7 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) | ||||
| 	}; | ||||
|  | ||||
| 	const login = async (identity: string, secret: string) => { | ||||
| 		const response = await getToken({ payload: { identity, secret } }); | ||||
| 		const response = await getToken(identity, secret); | ||||
| 		handleTokenUpdate(response); | ||||
| 	}; | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ export * from "./useAccessLists"; | ||||
| export * from "./useAuditLog"; | ||||
| export * from "./useAuditLogs"; | ||||
| export * from "./useCertificates"; | ||||
| export * from "./useDeadHost"; | ||||
| export * from "./useDeadHosts"; | ||||
| export * from "./useHealth"; | ||||
| export * from "./useHostReport"; | ||||
|   | ||||
							
								
								
									
										57
									
								
								frontend/src/hooks/useDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								frontend/src/hooks/useDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||||
| import { createDeadHost, type DeadHost, getDeadHost, updateDeadHost } from "src/api/backend"; | ||||
|  | ||||
| const fetchDeadHost = (id: number | "new") => { | ||||
| 	if (id === "new") { | ||||
| 		return Promise.resolve({ | ||||
| 			id: 0, | ||||
| 			createdOn: "", | ||||
| 			modifiedOn: "", | ||||
| 			ownerUserId: 0, | ||||
| 			domainNames: [], | ||||
| 			certificateId: 0, | ||||
| 			sslForced: false, | ||||
| 			advancedConfig: "", | ||||
| 			meta: {}, | ||||
| 			http2Support: false, | ||||
| 			enabled: true, | ||||
| 			hstsEnabled: false, | ||||
| 			hstsSubdomains: false, | ||||
| 		} as DeadHost); | ||||
| 	} | ||||
| 	return getDeadHost(id, ["owner"]); | ||||
| }; | ||||
|  | ||||
| const useDeadHost = (id: number | "new", options = {}) => { | ||||
| 	return useQuery<DeadHost, Error>({ | ||||
| 		queryKey: ["dead-host", id], | ||||
| 		queryFn: () => fetchDeadHost(id), | ||||
| 		staleTime: 60 * 1000, // 1 minute | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| const useSetDeadHost = () => { | ||||
| 	const queryClient = useQueryClient(); | ||||
| 	return useMutation({ | ||||
| 		mutationFn: (values: DeadHost) => (values.id ? updateDeadHost(values) : createDeadHost(values)), | ||||
| 		onMutate: (values: DeadHost) => { | ||||
| 			if (!values.id) { | ||||
| 				return; | ||||
| 			} | ||||
| 			const previousObject = queryClient.getQueryData(["dead-host", values.id]); | ||||
| 			queryClient.setQueryData(["dead-host", values.id], (old: DeadHost) => ({ | ||||
| 				...old, | ||||
| 				...values, | ||||
| 			})); | ||||
| 			return () => queryClient.setQueryData(["dead-host", values.id], previousObject); | ||||
| 		}, | ||||
| 		onError: (_, __, rollback: any) => rollback(), | ||||
| 		onSuccess: async ({ id }: DeadHost) => { | ||||
| 			queryClient.invalidateQueries({ queryKey: ["dead-host", id] }); | ||||
| 			queryClient.invalidateQueries({ queryKey: ["dead-hosts"] }); | ||||
| 		}, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { useDeadHost, useSetDeadHost }; | ||||
| @@ -1,11 +1,11 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { type DeadHost, type DeadHostExpansion, getDeadHosts } from "src/api/backend"; | ||||
| import { type DeadHost, getDeadHosts, type HostExpansion } from "src/api/backend"; | ||||
|  | ||||
| const fetchDeadHosts = (expand?: DeadHostExpansion[]) => { | ||||
| const fetchDeadHosts = (expand?: HostExpansion[]) => { | ||||
| 	return getDeadHosts(expand); | ||||
| }; | ||||
|  | ||||
| const useDeadHosts = (expand?: DeadHostExpansion[], options = {}) => { | ||||
| const useDeadHosts = (expand?: HostExpansion[], options = {}) => { | ||||
| 	return useQuery<DeadHost[], Error>({ | ||||
| 		queryKey: ["dead-hosts", { expand }], | ||||
| 		queryFn: () => fetchDeadHosts(expand), | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { getRedirectionHosts, type RedirectionHost, type RedirectionHostExpansion } from "src/api/backend"; | ||||
| import { getRedirectionHosts, type HostExpansion, type RedirectionHost } from "src/api/backend"; | ||||
|  | ||||
| const fetchRedirectionHosts = (expand?: RedirectionHostExpansion[]) => { | ||||
| const fetchRedirectionHosts = (expand?: HostExpansion[]) => { | ||||
| 	return getRedirectionHosts(expand); | ||||
| }; | ||||
|  | ||||
| const useRedirectionHosts = (expand?: RedirectionHostExpansion[], options = {}) => { | ||||
| const useRedirectionHosts = (expand?: HostExpansion[], options = {}) => { | ||||
| 	return useQuery<RedirectionHost[], Error>({ | ||||
| 		queryKey: ["redirection-hosts", { expand }], | ||||
| 		queryFn: () => fetchRedirectionHosts(expand), | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { getStreams, type Stream, type StreamExpansion } from "src/api/backend"; | ||||
| import { getStreams, type HostExpansion, type Stream } from "src/api/backend"; | ||||
|  | ||||
| const fetchStreams = (expand?: StreamExpansion[]) => { | ||||
| const fetchStreams = (expand?: HostExpansion[]) => { | ||||
| 	return getStreams(expand); | ||||
| }; | ||||
|  | ||||
| const useStreams = (expand?: StreamExpansion[], options = {}) => { | ||||
| const useStreams = (expand?: HostExpansion[], options = {}) => { | ||||
| 	return useQuery<Stream[], Error>({ | ||||
| 		queryKey: ["streams", { expand }], | ||||
| 		queryFn: () => fetchStreams(expand), | ||||
|   | ||||
| @@ -15,7 +15,7 @@ const fetchUser = (id: number | string) => { | ||||
| 			avatar: "", | ||||
| 		} as User); | ||||
| 	} | ||||
| 	return getUser(id, { expand: "permissions" }); | ||||
| 	return getUser(id, ["permissions"]); | ||||
| }; | ||||
|  | ||||
| const useUser = (id: string | number, options = {}) => { | ||||
|   | ||||
| @@ -24,6 +24,7 @@ | ||||
|   "column.access": "Access", | ||||
|   "column.authorization": "Authorization", | ||||
|   "column.destination": "Destination", | ||||
|   "column.details": "Details", | ||||
|   "column.email": "Email", | ||||
|   "column.event": "Event", | ||||
|   "column.expires": "Expires", | ||||
| @@ -40,12 +41,15 @@ | ||||
|   "column.status": "Status", | ||||
|   "created-on": "Created: {date}", | ||||
|   "dashboard.title": "Dashboard", | ||||
|   "dead-host.edit": "Edit 404 Host", | ||||
|   "dead-host.new": "New 404 Host", | ||||
|   "dead-hosts.actions-title": "404 Host #{id}", | ||||
|   "dead-hosts.add": "Add 404 Host", | ||||
|   "dead-hosts.count": "{count} 404 Hosts", | ||||
|   "dead-hosts.empty": "There are no 404 Hosts", | ||||
|   "dead-hosts.title": "404 Hosts", | ||||
|   "disabled": "Disabled", | ||||
|   "domain-names": "Domain Names", | ||||
|   "email-address": "Email address", | ||||
|   "empty-subtitle": "Why don't you create one?", | ||||
|   "error.invalid-auth": "Invalid email or password", | ||||
| @@ -96,6 +100,7 @@ | ||||
|   "setup.preamble": "Get started by creating your admin account.", | ||||
|   "setup.title": "Welcome!", | ||||
|   "sign-in": "Sign in", | ||||
|   "ssl-certificate": "SSL Certificate", | ||||
|   "streams.actions-title": "Stream #{id}", | ||||
|   "streams.add": "Add Stream", | ||||
|   "streams.count": "{count} Streams", | ||||
| @@ -122,5 +127,7 @@ | ||||
|   "user.switch-light": "Switch to Light mode", | ||||
|   "users.actions-title": "User #{id}", | ||||
|   "users.add": "Add User", | ||||
|   "users.title": "Users" | ||||
|   "users.title": "Users", | ||||
|   "wildcards-not-permitted": "Wildcards not permitted for this type", | ||||
|   "wildcards-not-supported": "Wildcards not supported for this CA" | ||||
| } | ||||
| @@ -77,6 +77,9 @@ | ||||
| 	"column.destination": { | ||||
| 		"defaultMessage": "Destination" | ||||
| 	}, | ||||
| 	"column.details": { | ||||
| 		"defaultMessage": "Details" | ||||
| 	}, | ||||
| 	"column.email": { | ||||
| 		"defaultMessage": "Email" | ||||
| 	}, | ||||
| @@ -131,15 +134,24 @@ | ||||
| 	"dead-hosts.count": { | ||||
| 		"defaultMessage": "{count} 404 Hosts" | ||||
| 	}, | ||||
| 	"dead-host.edit": { | ||||
| 		"defaultMessage": "Edit 404 Host" | ||||
| 	}, | ||||
| 	"dead-hosts.empty": { | ||||
| 		"defaultMessage": "There are no 404 Hosts" | ||||
| 	}, | ||||
| 	"dead-host.new": { | ||||
| 		"defaultMessage": "New 404 Host" | ||||
| 	}, | ||||
| 	"dead-hosts.title": { | ||||
| 		"defaultMessage": "404 Hosts" | ||||
| 	}, | ||||
| 	"disabled": { | ||||
| 		"defaultMessage": "Disabled" | ||||
| 	}, | ||||
| 	"domain-names": { | ||||
| 		"defaultMessage": "Domain Names" | ||||
| 	}, | ||||
| 	"email-address": { | ||||
| 		"defaultMessage": "Email address" | ||||
| 	}, | ||||
| @@ -290,6 +302,9 @@ | ||||
| 	"sign-in": { | ||||
| 		"defaultMessage": "Sign in" | ||||
| 	}, | ||||
| 	"ssl-certificate": { | ||||
| 		"defaultMessage": "SSL Certificate" | ||||
| 	}, | ||||
| 	"streams.actions-title": { | ||||
| 		"defaultMessage": "Stream #{id}" | ||||
| 	}, | ||||
| @@ -370,5 +385,11 @@ | ||||
| 	}, | ||||
| 	"users.title": { | ||||
| 		"defaultMessage": "Users" | ||||
| 	}, | ||||
| 	"wildcards-not-permitted": { | ||||
| 		"defaultMessage": "Wildcards not permitted for this type" | ||||
| 	}, | ||||
| 	"wildcards-not-supported": { | ||||
| 		"defaultMessage": "Wildcards not supported for this CA" | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										285
									
								
								frontend/src/modals/DeadHostModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								frontend/src/modals/DeadHostModal.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | ||||
| import { IconSettings } from "@tabler/icons-react"; | ||||
| import { Form, Formik } from "formik"; | ||||
| import { useState } from "react"; | ||||
| import { Alert } from "react-bootstrap"; | ||||
| import Modal from "react-bootstrap/Modal"; | ||||
| import { Button, DomainNamesField, Loading, SSLCertificateField } from "src/components"; | ||||
| import { useDeadHost } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	id: number | "new"; | ||||
| 	onClose: () => void; | ||||
| } | ||||
| export function DeadHostModal({ id, onClose }: Props) { | ||||
| 	const { data, isLoading, error } = useDeadHost(id); | ||||
| 	// const { mutate: setDeadHost } = useSetDeadHost(); | ||||
| 	const [errorMsg, setErrorMsg] = useState<string | null>(null); | ||||
|  | ||||
| 	const onSubmit = async (values: any, { setSubmitting }: any) => { | ||||
| 		setSubmitting(true); | ||||
| 		setErrorMsg(null); | ||||
| 		console.log("SUBMIT:", values); | ||||
| 		setSubmitting(false); | ||||
| 		// const { ...payload } = { | ||||
| 		// 	id: id === "new" ? undefined : id, | ||||
| 		// 	roles: [], | ||||
| 		// 	...values, | ||||
| 		// }; | ||||
|  | ||||
| 		// setDeadHost(payload, { | ||||
| 		// 	onError: (err: any) => setErrorMsg(err.message), | ||||
| 		// 	onSuccess: () => { | ||||
| 		// 		showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" })); | ||||
| 		// 		onClose(); | ||||
| 		// 	}, | ||||
| 		// 	onSettled: () => setSubmitting(false), | ||||
| 		// }); | ||||
| 	}; | ||||
|  | ||||
| 	return ( | ||||
| 		<Modal show onHide={onClose} animation={false}> | ||||
| 			{!isLoading && error && ( | ||||
| 				<Alert variant="danger" className="m-3"> | ||||
| 					{error?.message || "Unknown error"} | ||||
| 				</Alert> | ||||
| 			)} | ||||
| 			{isLoading && <Loading noLogo />} | ||||
| 			{!isLoading && data && ( | ||||
| 				<Formik | ||||
| 					initialValues={ | ||||
| 						{ | ||||
| 							domainNames: data?.domainNames, | ||||
| 							certificateId: data?.certificateId, | ||||
| 							sslForced: data?.sslForced, | ||||
| 							advancedConfig: data?.advancedConfig, | ||||
| 							http2Support: data?.http2Support, | ||||
| 							hstsEnabled: data?.hstsEnabled, | ||||
| 							hstsSubdomains: data?.hstsSubdomains, | ||||
| 						} as any | ||||
| 					} | ||||
| 					onSubmit={onSubmit} | ||||
| 				> | ||||
| 					{({ isSubmitting }) => ( | ||||
| 						<Form> | ||||
| 							<Modal.Header closeButton> | ||||
| 								<Modal.Title> | ||||
| 									{intl.formatMessage({ id: data?.id ? "dead-host.edit" : "dead-host.new" })} | ||||
| 								</Modal.Title> | ||||
| 							</Modal.Header> | ||||
| 							<Modal.Body className="p-0"> | ||||
| 								<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible> | ||||
| 									{errorMsg} | ||||
| 								</Alert> | ||||
|  | ||||
| 								<div className="card m-0 border-0"> | ||||
| 									<div className="card-header"> | ||||
| 										<ul className="nav nav-tabs card-header-tabs" data-bs-toggle="tabs"> | ||||
| 											<li className="nav-item" role="presentation"> | ||||
| 												<a | ||||
| 													href="#tab-details" | ||||
| 													className="nav-link active" | ||||
| 													data-bs-toggle="tab" | ||||
| 													aria-selected="true" | ||||
| 													role="tab" | ||||
| 												> | ||||
| 													{intl.formatMessage({ id: "column.details" })} | ||||
| 												</a> | ||||
| 											</li> | ||||
| 											<li className="nav-item" role="presentation"> | ||||
| 												<a | ||||
| 													href="#tab-ssl" | ||||
| 													className="nav-link" | ||||
| 													data-bs-toggle="tab" | ||||
| 													aria-selected="false" | ||||
| 													tabIndex={-1} | ||||
| 													role="tab" | ||||
| 												> | ||||
| 													{intl.formatMessage({ id: "column.ssl" })} | ||||
| 												</a> | ||||
| 											</li> | ||||
| 											<li className="nav-item ms-auto" role="presentation"> | ||||
| 												<a | ||||
| 													href="#tab-advanced" | ||||
| 													className="nav-link" | ||||
| 													title="Settings" | ||||
| 													data-bs-toggle="tab" | ||||
| 													aria-selected="false" | ||||
| 													tabIndex={-1} | ||||
| 													role="tab" | ||||
| 												> | ||||
| 													<IconSettings size={20} /> | ||||
| 												</a> | ||||
| 											</li> | ||||
| 										</ul> | ||||
| 									</div> | ||||
| 									<div className="card-body"> | ||||
| 										<div className="tab-content"> | ||||
| 											<div className="tab-pane active show" id="tab-details" role="tabpanel"> | ||||
| 												<DomainNamesField isWildcardPermitted /> | ||||
| 											</div> | ||||
| 											<div className="tab-pane" id="tab-ssl" role="tabpanel"> | ||||
| 												<SSLCertificateField | ||||
| 													name="certificateId" | ||||
| 													label="ssl-certificate" | ||||
| 													allowNew | ||||
| 												/> | ||||
| 											</div> | ||||
| 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | ||||
| 												<h4>Advanced</h4> | ||||
| 											</div> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								</div> | ||||
|  | ||||
| 								{/* <div className="row"> | ||||
| 									<div className="col-lg-6"> | ||||
| 										<div className="mb-3"> | ||||
| 											<Field name="name" validate={validateString(1, 50)}> | ||||
| 												{({ field, form }: any) => ( | ||||
| 													<div className="form-floating mb-3"> | ||||
| 														<input | ||||
| 															id="name" | ||||
| 															className={`form-control ${form.errors.name && form.touched.name ? "is-invalid" : ""}`} | ||||
| 															placeholder={intl.formatMessage({ id: "user.full-name" })} | ||||
| 															{...field} | ||||
| 														/> | ||||
| 														<label htmlFor="name"> | ||||
| 															{intl.formatMessage({ id: "user.full-name" })} | ||||
| 														</label> | ||||
| 														{form.errors.name ? ( | ||||
| 															<div className="invalid-feedback"> | ||||
| 																{form.errors.name && form.touched.name | ||||
| 																	? form.errors.name | ||||
| 																	: null} | ||||
| 															</div> | ||||
| 														) : null} | ||||
| 													</div> | ||||
| 												)} | ||||
| 											</Field> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 									<div className="col-lg-6"> | ||||
| 										<div className="mb-3"> | ||||
| 											<Field name="nickname" validate={validateString(1, 30)}> | ||||
| 												{({ field, form }: any) => ( | ||||
| 													<div className="form-floating mb-3"> | ||||
| 														<input | ||||
| 															id="nickname" | ||||
| 															className={`form-control ${form.errors.nickname && form.touched.nickname ? "is-invalid" : ""}`} | ||||
| 															placeholder={intl.formatMessage({ id: "user.nickname" })} | ||||
| 															{...field} | ||||
| 														/> | ||||
| 														<label htmlFor="nickname"> | ||||
| 															{intl.formatMessage({ id: "user.nickname" })} | ||||
| 														</label> | ||||
| 														{form.errors.nickname ? ( | ||||
| 															<div className="invalid-feedback"> | ||||
| 																{form.errors.nickname && form.touched.nickname | ||||
| 																	? form.errors.nickname | ||||
| 																	: null} | ||||
| 															</div> | ||||
| 														) : null} | ||||
| 													</div> | ||||
| 												)} | ||||
| 											</Field> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 								<div className="mb-3"> | ||||
| 									<Field name="email" validate={validateEmail()}> | ||||
| 										{({ field, form }: any) => ( | ||||
| 											<div className="form-floating mb-3"> | ||||
| 												<input | ||||
| 													id="email" | ||||
| 													type="email" | ||||
| 													className={`form-control ${form.errors.email && form.touched.email ? "is-invalid" : ""}`} | ||||
| 													placeholder={intl.formatMessage({ id: "email-address" })} | ||||
| 													{...field} | ||||
| 												/> | ||||
| 												<label htmlFor="email"> | ||||
| 													{intl.formatMessage({ id: "email-address" })} | ||||
| 												</label> | ||||
| 												{form.errors.email ? ( | ||||
| 													<div className="invalid-feedback"> | ||||
| 														{form.errors.email && form.touched.email | ||||
| 															? form.errors.email | ||||
| 															: null} | ||||
| 													</div> | ||||
| 												) : null} | ||||
| 											</div> | ||||
| 										)} | ||||
| 									</Field> | ||||
| 								</div> | ||||
| 								{currentUser && data && currentUser?.id !== data?.id ? ( | ||||
| 									<div className="my-3"> | ||||
| 										<h3 className="py-2">{intl.formatMessage({ id: "user.flags.title" })}</h3> | ||||
| 										<div className="divide-y"> | ||||
| 											<div> | ||||
| 												<label className="row" htmlFor="isAdmin"> | ||||
| 													<span className="col"> | ||||
| 														{intl.formatMessage({ id: "role.admin" })} | ||||
| 													</span> | ||||
| 													<span className="col-auto"> | ||||
| 														<Field name="isAdmin" type="checkbox"> | ||||
| 															{({ field }: any) => ( | ||||
| 																<label className="form-check form-check-single form-switch"> | ||||
| 																	<input | ||||
| 																		{...field} | ||||
| 																		id="isAdmin" | ||||
| 																		className="form-check-input" | ||||
| 																		type="checkbox" | ||||
| 																	/> | ||||
| 																</label> | ||||
| 															)} | ||||
| 														</Field> | ||||
| 													</span> | ||||
| 												</label> | ||||
| 											</div> | ||||
| 											<div> | ||||
| 												<label className="row" htmlFor="isDisabled"> | ||||
| 													<span className="col"> | ||||
| 														{intl.formatMessage({ id: "disabled" })} | ||||
| 													</span> | ||||
| 													<span className="col-auto"> | ||||
| 														<Field name="isDisabled" type="checkbox"> | ||||
| 															{({ field }: any) => ( | ||||
| 																<label className="form-check form-check-single form-switch"> | ||||
| 																	<input | ||||
| 																		{...field} | ||||
| 																		id="isDisabled" | ||||
| 																		className="form-check-input" | ||||
| 																		type="checkbox" | ||||
| 																	/> | ||||
| 																</label> | ||||
| 															)} | ||||
| 														</Field> | ||||
| 													</span> | ||||
| 												</label> | ||||
| 											</div> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								) : null} */} | ||||
| 							</Modal.Body> | ||||
| 							<Modal.Footer> | ||||
| 								<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> | ||||
| 									{intl.formatMessage({ id: "cancel" })} | ||||
| 								</Button> | ||||
| 								<Button | ||||
| 									type="submit" | ||||
| 									actionType="primary" | ||||
| 									className="ms-auto" | ||||
| 									data-bs-dismiss="modal" | ||||
| 									isLoading={isSubmitting} | ||||
| 									disabled={isSubmitting} | ||||
| 								> | ||||
| 									{intl.formatMessage({ id: "save" })} | ||||
| 								</Button> | ||||
| 							</Modal.Footer> | ||||
| 						</Form> | ||||
| 					)} | ||||
| 				</Formik> | ||||
| 			)} | ||||
| 		</Modal> | ||||
| 	); | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| export * from "./ChangePasswordModal"; | ||||
| export * from "./DeadHostModal"; | ||||
| export * from "./DeleteConfirmModal"; | ||||
| export * from "./EventDetailsModal"; | ||||
| export * from "./PermissionsModal"; | ||||
|   | ||||
| @@ -1,132 +0,0 @@ | ||||
| import { IconDotsVertical, IconEdit, IconPower, IconSearch, IconTrash } from "@tabler/icons-react"; | ||||
| import { Button } from "src/components"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| export default function CertificateTable() { | ||||
| 	return ( | ||||
| 		<div className="card mt-4"> | ||||
| 			<div className="card-status-top bg-pink" /> | ||||
| 			<div className="card-table"> | ||||
| 				<div className="card-header"> | ||||
| 					<div className="row w-full"> | ||||
| 						<div className="col"> | ||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "certificates.title" })}</h2> | ||||
| 						</div> | ||||
| 						<div className="col-md-auto col-sm-12"> | ||||
| 							<div className="ms-auto d-flex flex-wrap btn-list"> | ||||
| 								<div className="input-group input-group-flat w-auto"> | ||||
| 									<span className="input-group-text input-group-text-sm"> | ||||
| 										<IconSearch size={16} /> | ||||
| 									</span> | ||||
| 									<input | ||||
| 										id="advanced-table-search" | ||||
| 										type="text" | ||||
| 										className="form-control form-control-sm" | ||||
| 										autoComplete="off" | ||||
| 									/> | ||||
| 								</div> | ||||
| 								<Button size="sm" className="btn-pink"> | ||||
| 									Add Certificate (dropdown) | ||||
| 								</Button> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div id="advanced-table"> | ||||
| 					<div className="table-responsive"> | ||||
| 						<table className="table table-vcenter table-selectable"> | ||||
| 							<thead> | ||||
| 								<tr> | ||||
| 									<th className="w-1" /> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											Source | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											Destination | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											SSL | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											Access | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th> | ||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> | ||||
| 											Status | ||||
| 										</button> | ||||
| 									</th> | ||||
| 									<th className="w-1" /> | ||||
| 								</tr> | ||||
| 							</thead> | ||||
| 							<tbody className="table-tbody"> | ||||
| 								<tr> | ||||
| 									<td data-label="Owner"> | ||||
| 										<div className="d-flex py-1 align-items-center"> | ||||
| 											<span | ||||
| 												className="avatar avatar-2 me-2" | ||||
| 												style={{ | ||||
| 													backgroundImage: | ||||
| 														"url(//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm)", | ||||
| 												}} | ||||
| 											/> | ||||
| 										</div> | ||||
| 									</td> | ||||
| 									<td data-label="Destination"> | ||||
| 										<div className="flex-fill"> | ||||
| 											<div className="font-weight-medium"> | ||||
| 												<span className="badge badge-lg domain-name">blog.jc21.com</span> | ||||
| 											</div> | ||||
| 											<div className="text-secondary mt-1">Created: 20th September 2024</div> | ||||
| 										</div> | ||||
| 									</td> | ||||
| 									<td data-label="Source">http://172.17.0.1:3001</td> | ||||
| 									<td data-label="SSL">Let's Encrypt</td> | ||||
| 									<td data-label="Access">Public</td> | ||||
| 									<td data-label="Status"> | ||||
| 										<span className="badge bg-lime-lt">Online</span> | ||||
| 									</td> | ||||
| 									<td data-label="Status" className="text-end"> | ||||
| 										<span className="dropdown"> | ||||
| 											<button | ||||
| 												type="button" | ||||
| 												className="btn dropdown-toggle btn-action btn-sm px-1" | ||||
| 												data-bs-boundary="viewport" | ||||
| 												data-bs-toggle="dropdown" | ||||
| 											> | ||||
| 												<IconDotsVertical /> | ||||
| 											</button> | ||||
| 											<div className="dropdown-menu dropdown-menu-end"> | ||||
| 												<span className="dropdown-header">Proxy Host #2</span> | ||||
| 												<a className="dropdown-item" href="#"> | ||||
| 													<IconEdit size={16} /> | ||||
| 													Edit | ||||
| 												</a> | ||||
| 												<a className="dropdown-item" href="#"> | ||||
| 													<IconPower size={16} /> | ||||
| 													Disable | ||||
| 												</a> | ||||
| 												<div className="dropdown-divider" /> | ||||
| 												<a className="dropdown-item" href="#"> | ||||
| 													<IconTrash size={16} /> | ||||
| 													Delete | ||||
| 												</a> | ||||
| 											</div> | ||||
| 										</span> | ||||
| 									</td> | ||||
| 								</tr> | ||||
| 							</tbody> | ||||
| 						</table> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
| @@ -4,15 +4,18 @@ import { intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	tableInstance: ReactTable<any>; | ||||
| 	onNew?: () => void; | ||||
| } | ||||
| export default function Empty({ tableInstance }: Props) { | ||||
| export default function Empty({ tableInstance, onNew }: Props) { | ||||
| 	return ( | ||||
| 		<tr> | ||||
| 			<td colSpan={tableInstance.getVisibleFlatColumns().length}> | ||||
| 				<div className="text-center my-4"> | ||||
| 					<h2>{intl.formatMessage({ id: "dead-hosts.empty" })}</h2> | ||||
| 					<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p> | ||||
| 					<Button className="btn-red my-3">{intl.formatMessage({ id: "dead-hosts.add" })}</Button> | ||||
| 					<Button className="btn-red my-3" onClick={onNew}> | ||||
| 						{intl.formatMessage({ id: "dead-hosts.add" })} | ||||
| 					</Button> | ||||
| 				</div> | ||||
| 			</td> | ||||
| 		</tr> | ||||
|   | ||||
| @@ -10,8 +10,10 @@ import Empty from "./Empty"; | ||||
| interface Props { | ||||
| 	data: DeadHost[]; | ||||
| 	isFetching?: boolean; | ||||
| 	onDelete?: (id: number) => void; | ||||
| 	onNew?: () => void; | ||||
| } | ||||
| export default function Table({ data, isFetching }: Props) { | ||||
| export default function Table({ data, isFetching, onDelete, onNew }: Props) { | ||||
| 	const columnHelper = createColumnHelper<DeadHost>(); | ||||
| 	const columns = useMemo( | ||||
| 		() => [ | ||||
| @@ -78,7 +80,14 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 									{intl.formatMessage({ id: "action.disable" })} | ||||
| 								</a> | ||||
| 								<div className="dropdown-divider" /> | ||||
| 								<a className="dropdown-item" href="#"> | ||||
| 								<a | ||||
| 									className="dropdown-item" | ||||
| 									href="#" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										onDelete?.(info.row.original.id); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconTrash size={16} /> | ||||
| 									{intl.formatMessage({ id: "action.delete" })} | ||||
| 								</a> | ||||
| @@ -91,7 +100,7 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 				}, | ||||
| 			}), | ||||
| 		], | ||||
| 		[columnHelper], | ||||
| 		[columnHelper, onDelete], | ||||
| 	); | ||||
|  | ||||
| 	const tableInstance = useReactTable<DeadHost>({ | ||||
| @@ -105,5 +114,7 @@ export default function Table({ data, isFetching }: Props) { | ||||
| 		enableSortingRemoval: false, | ||||
| 	}); | ||||
|  | ||||
| 	return <TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} />} />; | ||||
| 	return ( | ||||
| 		<TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} onNew={onNew} />} /> | ||||
| 	); | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,16 @@ | ||||
| import { IconSearch } from "@tabler/icons-react"; | ||||
| import { useState } from "react"; | ||||
| import Alert from "react-bootstrap/Alert"; | ||||
| import { Button, LoadingPage } from "src/components"; | ||||
| import { useDeadHosts } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
| import { DeadHostModal, DeleteConfirmModal } from "src/modals"; | ||||
| import { showSuccess } from "src/notifications"; | ||||
| import Table from "./Table"; | ||||
|  | ||||
| export default function TableWrapper() { | ||||
| 	const [deleteId, setDeleteId] = useState(0); | ||||
| 	const [editId, setEditId] = useState(0 as number | "new"); | ||||
| 	const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]); | ||||
|  | ||||
| 	if (isLoading) { | ||||
| @@ -16,6 +21,11 @@ export default function TableWrapper() { | ||||
| 		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; | ||||
| 	} | ||||
|  | ||||
| 	const handleDelete = async () => { | ||||
| 		// await deleteUser(deleteId); | ||||
| 		showSuccess(intl.formatMessage({ id: "notification.host-deleted" })); | ||||
| 	}; | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="card mt-4"> | ||||
| 			<div className="card-status-top bg-red" /> | ||||
| @@ -38,14 +48,30 @@ export default function TableWrapper() { | ||||
| 										autoComplete="off" | ||||
| 									/> | ||||
| 								</div> | ||||
| 								<Button size="sm" className="btn-red"> | ||||
| 								<Button size="sm" className="btn-red" onClick={() => setEditId("new")}> | ||||
| 									{intl.formatMessage({ id: "dead-hosts.add" })} | ||||
| 								</Button> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<Table data={data ?? []} isFetching={isFetching} /> | ||||
| 				<Table | ||||
| 					data={data ?? []} | ||||
| 					isFetching={isFetching} | ||||
| 					onDelete={(id: number) => setDeleteId(id)} | ||||
| 					onNew={() => setEditId("new")} | ||||
| 				/> | ||||
| 				{editId ? <DeadHostModal id={editId} onClose={() => setEditId(0)} /> : null} | ||||
| 				{deleteId ? ( | ||||
| 					<DeleteConfirmModal | ||||
| 						title={intl.formatMessage({ id: "user.delete.title" })} | ||||
| 						onConfirm={handleDelete} | ||||
| 						onClose={() => setDeleteId(0)} | ||||
| 						invalidations={[["dead-hosts"], ["dead-host", deleteId]]} | ||||
| 					> | ||||
| 						{intl.formatMessage({ id: "user.delete.content" })} | ||||
| 					</DeleteConfirmModal> | ||||
| 				) : null} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user