mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-09 05:58:17 +02:00
add no auth and passthrough auth
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { AiProviderAuthTypeSelect } from "@app/components/AiProviderAuthTypeSelect";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
@@ -23,13 +24,6 @@ import {
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { useAiProviderContext } from "@app/hooks/useAiProviderContext";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
@@ -40,9 +34,10 @@ import {
|
||||
type AiProviderFormValues
|
||||
} from "@app/lib/aiProviderFormSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type {
|
||||
AiProviderAuthType,
|
||||
AiProviderType
|
||||
import {
|
||||
authTypeRequiresApiKey,
|
||||
type AiProviderAuthType,
|
||||
type AiProviderType
|
||||
} from "@server/lib/aiProviderDefaults";
|
||||
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
|
||||
import type { AxiosResponse } from "axios";
|
||||
@@ -73,7 +68,10 @@ export default function AiProviderAuthenticationPage() {
|
||||
}
|
||||
});
|
||||
|
||||
const showAuthType = provider.type === "custom";
|
||||
const authType = form.watch("authType");
|
||||
const showApiKey = authTypeRequiresApiKey(
|
||||
(authType as AiProviderAuthType | null) ?? "bearer"
|
||||
);
|
||||
|
||||
async function onSubmit(values: AiProviderFormValues) {
|
||||
setSaveLoading(true);
|
||||
@@ -135,88 +133,22 @@ export default function AiProviderAuthenticationPage() {
|
||||
id="ai-provider-auth-form"
|
||||
>
|
||||
<SettingsFormGrid>
|
||||
{showAuthType && (
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"aiProviderAuthType"
|
||||
)}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={
|
||||
field.value ??
|
||||
"bearer"
|
||||
}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="bearer">
|
||||
{t(
|
||||
"aiProviderAuthTypeBearer"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="x-api-key">
|
||||
{t(
|
||||
"aiProviderAuthTypeXApiKey"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="x-goog-api-key">
|
||||
{t(
|
||||
"aiProviderAuthTypeXGoogApiKey"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="hec">
|
||||
{t(
|
||||
"aiProviderAuthTypeHec"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="cf-aig-authorization">
|
||||
{t(
|
||||
"aiProviderAuthTypeCfAigAuthorization"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiProviderAuthTypeDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
name="authType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("aiProviderApiKey")}
|
||||
{t(
|
||||
"aiProviderAuthType"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
<AiProviderAuthTypeSelect
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
"bearer"
|
||||
}
|
||||
onChange={
|
||||
field.onChange
|
||||
@@ -225,7 +157,7 @@ export default function AiProviderAuthenticationPage() {
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiProviderApiKeyDescription"
|
||||
"aiProviderAuthTypeDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -233,6 +165,43 @@ export default function AiProviderAuthenticationPage() {
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
{showApiKey && (
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"aiProviderApiKey"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiProviderApiKeyDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
SettingsSubsectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import { AiProviderAuthTypeSelect } from "@app/components/AiProviderAuthTypeSelect";
|
||||
import { AiProviderTypeSelect } from "@app/components/AiProviderTypeSelect";
|
||||
import { StrategySelect } from "@app/components/StrategySelect";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
@@ -33,18 +34,12 @@ import {
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import {
|
||||
aiProviderCreateFormSchema,
|
||||
defaultAuthTypeForProvider,
|
||||
emptyUpstreamForType,
|
||||
showsUpstreamUrlField,
|
||||
toAiProviderCreatePayload,
|
||||
@@ -52,6 +47,7 @@ import {
|
||||
type AiProviderFormValues
|
||||
} from "@app/lib/aiProviderFormSchema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { authTypeRequiresApiKey } from "@server/lib/aiProviderDefaults";
|
||||
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -76,7 +72,7 @@ export default function CreateAiProviderPage() {
|
||||
type: "openai",
|
||||
upstreamUrl: emptyUpstreamForType("openai"),
|
||||
apiKey: "",
|
||||
authType: "bearer",
|
||||
authType: defaultAuthTypeForProvider("openai"),
|
||||
routingMode: "url",
|
||||
skipTlsVerification: false,
|
||||
enabled: true
|
||||
@@ -85,12 +81,13 @@ export default function CreateAiProviderPage() {
|
||||
|
||||
const providerType = form.watch("type");
|
||||
const routingMode = form.watch("routingMode");
|
||||
const authType = form.watch("authType");
|
||||
|
||||
const showUpstream = showsUpstreamUrlField(providerType, routingMode);
|
||||
const requireUpstream = upstreamUrlRequired(providerType, routingMode);
|
||||
const showRoutingMode = providerType === "custom";
|
||||
const showAuthType = providerType === "custom";
|
||||
const showTargets = providerType === "custom" && routingMode === "target";
|
||||
const showApiKey = authTypeRequiresApiKey(authType ?? "bearer");
|
||||
|
||||
async function createTargets(
|
||||
providerId: number,
|
||||
@@ -272,6 +269,12 @@ export default function CreateAiProviderPage() {
|
||||
value
|
||||
)
|
||||
);
|
||||
form.setValue(
|
||||
"authType",
|
||||
defaultAuthTypeForProvider(
|
||||
value
|
||||
)
|
||||
);
|
||||
if (
|
||||
value !==
|
||||
"custom"
|
||||
@@ -495,88 +498,22 @@ export default function CreateAiProviderPage() {
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
<SettingsFormGrid>
|
||||
{showAuthType && (
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"aiProviderAuthType"
|
||||
)}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={
|
||||
field.value ??
|
||||
"bearer"
|
||||
}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="bearer">
|
||||
{t(
|
||||
"aiProviderAuthTypeBearer"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="x-api-key">
|
||||
{t(
|
||||
"aiProviderAuthTypeXApiKey"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="x-goog-api-key">
|
||||
{t(
|
||||
"aiProviderAuthTypeXGoogApiKey"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="hec">
|
||||
{t(
|
||||
"aiProviderAuthTypeHec"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="cf-aig-authorization">
|
||||
{t(
|
||||
"aiProviderAuthTypeCfAigAuthorization"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiProviderAuthTypeDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
name="authType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("aiProviderApiKey")}
|
||||
{t(
|
||||
"aiProviderAuthType"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
<AiProviderAuthTypeSelect
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
"bearer"
|
||||
}
|
||||
onChange={
|
||||
field.onChange
|
||||
@@ -585,7 +522,7 @@ export default function CreateAiProviderPage() {
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiProviderApiKeyDescription"
|
||||
"aiProviderAuthTypeDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -593,6 +530,43 @@ export default function CreateAiProviderPage() {
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
{showApiKey && (
|
||||
<SettingsFormCell span="half">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"aiProviderApiKey"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiProviderApiKeyDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</SettingsFormGrid>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from "@app/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@app/components/ui/popover";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import {
|
||||
AI_PROVIDER_AUTH_TYPES,
|
||||
type AiProviderAuthType
|
||||
} from "@server/lib/aiProviderDefaults";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const authLabelMap = {
|
||||
bearer: "aiProviderAuthTypeBearer",
|
||||
"x-api-key": "aiProviderAuthTypeXApiKey",
|
||||
"x-goog-api-key": "aiProviderAuthTypeXGoogApiKey",
|
||||
hec: "aiProviderAuthTypeHec",
|
||||
"cf-aig-authorization": "aiProviderAuthTypeCfAigAuthorization",
|
||||
none: "aiProviderAuthTypeNone",
|
||||
passthrough: "aiProviderAuthTypePassthrough"
|
||||
} as const;
|
||||
|
||||
const authDescriptionMap = {
|
||||
bearer: "aiProviderAuthTypeBearerDescription",
|
||||
"x-api-key": "aiProviderAuthTypeXApiKeyDescription",
|
||||
"x-goog-api-key": "aiProviderAuthTypeXGoogApiKeyDescription",
|
||||
hec: "aiProviderAuthTypeHecDescription",
|
||||
"cf-aig-authorization": "aiProviderAuthTypeCfAigAuthorizationDescription",
|
||||
none: "aiProviderAuthTypeNoneDescription",
|
||||
passthrough: "aiProviderAuthTypePassthroughDescription"
|
||||
} as const;
|
||||
|
||||
type AiProviderAuthTypeSelectProps = {
|
||||
value: AiProviderAuthType;
|
||||
onChange: (value: AiProviderAuthType) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AiProviderAuthTypeSelect({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
className
|
||||
}: AiProviderAuthTypeSelectProps) {
|
||||
const t = useTranslations();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
AI_PROVIDER_AUTH_TYPES.map((authType) => ({
|
||||
authType,
|
||||
title: t(authLabelMap[authType]),
|
||||
description: t(authDescriptionMap[authType])
|
||||
})),
|
||||
[t]
|
||||
);
|
||||
|
||||
const selected = options.find((option) => option.authType === value);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
!selected && "text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="truncate text-left">
|
||||
{selected?.title ?? t("noneSelected")}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder={t("aiProviderAuthTypeSearch")} />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t("aiProviderAuthTypeNotFound")}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.authType}
|
||||
value={`${option.authType} ${option.title} ${option.description}`}
|
||||
onSelect={() => {
|
||||
onChange(option.authType);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4 shrink-0",
|
||||
option.authType === value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="truncate">
|
||||
{option.title}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs leading-snug">
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import { z } from "zod";
|
||||
import {
|
||||
AI_PROVIDER_AUTH_TYPES,
|
||||
AI_PROVIDER_DEFAULTS,
|
||||
authTypeRequiresApiKey,
|
||||
providerRequiresUpstreamUrl,
|
||||
type AiProviderAuthType,
|
||||
type AiProviderType
|
||||
} from "@server/lib/aiProviderDefaults";
|
||||
|
||||
@@ -70,10 +72,10 @@ export const aiProviderFormSchema = z
|
||||
});
|
||||
}
|
||||
|
||||
if (data.type === "custom" && !data.authType) {
|
||||
if (!data.authType) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "authType is required for custom providers",
|
||||
message: "authType is required",
|
||||
path: ["authType"]
|
||||
});
|
||||
}
|
||||
@@ -83,7 +85,9 @@ export type AiProviderFormValues = z.infer<typeof aiProviderFormSchema>;
|
||||
|
||||
export const aiProviderCreateFormSchema = aiProviderFormSchema.superRefine(
|
||||
(data, ctx) => {
|
||||
if (!data.apiKey?.trim()) {
|
||||
const authType: AiProviderAuthType = data.authType ?? "bearer";
|
||||
|
||||
if (authTypeRequiresApiKey(authType) && !data.apiKey?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "API key is required",
|
||||
@@ -93,6 +97,15 @@ export const aiProviderCreateFormSchema = aiProviderFormSchema.superRefine(
|
||||
}
|
||||
);
|
||||
|
||||
export function defaultAuthTypeForProvider(
|
||||
type: AiProviderType
|
||||
): AiProviderAuthType {
|
||||
if (type === "custom") {
|
||||
return "bearer";
|
||||
}
|
||||
return AI_PROVIDER_DEFAULTS[type].authType;
|
||||
}
|
||||
|
||||
export function emptyUpstreamForType(type: AiProviderType): string {
|
||||
if (type === "custom") {
|
||||
return "";
|
||||
@@ -136,10 +149,7 @@ export function toAiProviderCreatePayload(values: AiProviderFormValues) {
|
||||
routingMode: values.type === "custom" ? routingMode : undefined,
|
||||
upstreamUrl,
|
||||
apiKey: values.apiKey?.trim() ? values.apiKey.trim() : undefined,
|
||||
authType:
|
||||
values.type === "custom"
|
||||
? (values.authType ?? "bearer")
|
||||
: undefined,
|
||||
authType: values.authType ?? "bearer",
|
||||
skipTlsVerification: values.skipTlsVerification,
|
||||
enabled: values.enabled ?? true
|
||||
};
|
||||
@@ -160,14 +170,11 @@ export function toAiProviderUpdatePayload(values: AiProviderFormValues) {
|
||||
name: values.name.trim(),
|
||||
routingMode: values.type === "custom" ? routingMode : "url",
|
||||
upstreamUrl,
|
||||
authType: values.authType ?? "bearer",
|
||||
skipTlsVerification: values.skipTlsVerification ?? false,
|
||||
enabled: values.enabled ?? true
|
||||
};
|
||||
|
||||
if (values.type === "custom") {
|
||||
payload.authType = values.authType ?? "bearer";
|
||||
}
|
||||
|
||||
if (values.apiKey?.trim()) {
|
||||
payload.apiKey = values.apiKey.trim();
|
||||
}
|
||||
@@ -185,13 +192,10 @@ export function toAiProviderNetworkPayload(values: AiProviderFormValues) {
|
||||
}
|
||||
|
||||
export function toAiProviderAuthPayload(values: AiProviderFormValues) {
|
||||
const payload: Record<string, unknown> = {
|
||||
return {
|
||||
authType: values.authType ?? "bearer",
|
||||
...(values.apiKey !== undefined ? { apiKey: values.apiKey.trim() } : {})
|
||||
};
|
||||
if (values.type === "custom") {
|
||||
payload.authType = values.authType ?? "bearer";
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function toAiProviderConfigurationPayload(values: AiProviderFormValues) {
|
||||
|
||||
Reference in New Issue
Block a user