mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-02 12:12:08 +03:00
feat(panel): add sponsor slots fed from sponsors.sanaei.dev
Monthly sponsor placements need to change without cutting a panel release. Panels now read 3X/sponsors.json from the MHSanaei/sponsors repo (GitHub Pages on sponsors.sanaei.dev) and show active sponsors in four slots: an overview banner, a rotating sidebar card (max three), the login page and a new Sponsors page that also lists open placements. An entry shows only while enable is not false and until is in the future; links must be https and logos are png/webp/jpg by name only. The list is cached for an hour and the last good copy survives upstream failures; logos are proxied through /sponsors/logo/:name with failures cached, so CSP stays 'self' and admin browsers never reach a third party. Admins can hide a slot for 24h. Under XUI_DEBUG the panel reads a sibling ../sponsors/3X checkout so edits can be previewed before push.
This commit is contained in:
@@ -1019,6 +1019,37 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"key": "",
|
||||
"value": ""
|
||||
},
|
||||
"Sponsor": {
|
||||
"enable": true,
|
||||
"id": "acme-2026-10",
|
||||
"link": "https://acme.example/?utm_source=3x-ui",
|
||||
"logo": "/sponsors/logo/acme.png",
|
||||
"name": "Acme VPS",
|
||||
"slots": [
|
||||
""
|
||||
],
|
||||
"text": {},
|
||||
"title": {},
|
||||
"until": "2026-11-01T00:00:00Z"
|
||||
},
|
||||
"SponsorList": {
|
||||
"contact": "https://t.me/example",
|
||||
"sponsors": [
|
||||
{
|
||||
"enable": true,
|
||||
"id": "acme-2026-10",
|
||||
"link": "https://acme.example/?utm_source=3x-ui",
|
||||
"logo": "/sponsors/logo/acme.png",
|
||||
"name": "Acme VPS",
|
||||
"slots": [
|
||||
""
|
||||
],
|
||||
"text": {},
|
||||
"title": {},
|
||||
"until": "2026-11-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubBalancer": {
|
||||
"createdAt": 1710000000000,
|
||||
"enabled": true,
|
||||
|
||||
@@ -4109,6 +4109,84 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Sponsor": {
|
||||
"description": "Sponsor is one paid placement published in the repo's sponsors.json.",
|
||||
"properties": {
|
||||
"enable": {
|
||||
"example": true,
|
||||
"nullable": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"example": "acme-2026-10",
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"example": "https://acme.example/?utm_source=3x-ui",
|
||||
"type": "string"
|
||||
},
|
||||
"logo": {
|
||||
"example": "/sponsors/logo/acme.png",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"example": "Acme VPS",
|
||||
"type": "string"
|
||||
},
|
||||
"slots": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"text": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"title": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"until": {
|
||||
"example": "2026-11-01T00:00:00Z",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"link",
|
||||
"name",
|
||||
"slots",
|
||||
"text",
|
||||
"title",
|
||||
"until"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SponsorList": {
|
||||
"description": "SponsorList is the active sponsor set plus the contact link for new sponsors.",
|
||||
"properties": {
|
||||
"contact": {
|
||||
"example": "https://t.me/example",
|
||||
"type": "string"
|
||||
},
|
||||
"sponsors": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Sponsor"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sponsors"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubBalancer": {
|
||||
"description": "SubBalancer is one extra JSON-subscription config document whose members are\nthe selected inbounds' proxy outbounds. SortOrder shares SubSortIndex semantics.",
|
||||
"properties": {
|
||||
|
||||
@@ -939,6 +939,23 @@ export interface Setting {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Sponsor {
|
||||
enable?: boolean | null;
|
||||
id: string;
|
||||
link: string;
|
||||
logo?: string;
|
||||
name: string;
|
||||
slots: string[];
|
||||
text: Record<string, string>;
|
||||
title: Record<string, string>;
|
||||
until: string;
|
||||
}
|
||||
|
||||
export interface SponsorList {
|
||||
contact?: string;
|
||||
sponsors: Sponsor[];
|
||||
}
|
||||
|
||||
export interface SubBalancer {
|
||||
createdAt: number;
|
||||
enabled: boolean;
|
||||
|
||||
@@ -998,6 +998,25 @@ export const SettingSchema = z.object({
|
||||
});
|
||||
export type Setting = z.infer<typeof SettingSchema>;
|
||||
|
||||
export const SponsorSchema = z.object({
|
||||
enable: z.boolean().nullable().optional(),
|
||||
id: z.string(),
|
||||
link: z.string(),
|
||||
logo: z.string().optional(),
|
||||
name: z.string(),
|
||||
slots: z.array(z.string()),
|
||||
text: z.record(z.string(), z.string()),
|
||||
title: z.record(z.string(), z.string()),
|
||||
until: z.string(),
|
||||
});
|
||||
export type Sponsor = z.infer<typeof SponsorSchema>;
|
||||
|
||||
export const SponsorListSchema = z.object({
|
||||
contact: z.string().optional(),
|
||||
sponsors: z.array(z.lazy(() => SponsorSchema)),
|
||||
});
|
||||
export type SponsorList = z.infer<typeof SponsorListSchema>;
|
||||
|
||||
export const SubBalancerSchema = z.object({
|
||||
createdAt: z.number().int(),
|
||||
enabled: z.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user