前端:创建api,主题,路由服务,重新定义主题插件的约束,错误,加载组件
后端:去除文章和模板的自义定路径,创建获取系统令牌api
This commit is contained in:
parent
f5754f982f
commit
5ca72e42cf
@ -32,7 +32,6 @@ CREATE TABLE pages
|
||||
page_content TEXT NOT NULL, --- 独立页面内容
|
||||
page_mould VARCHAR(50), --- 独立页面模板名称
|
||||
page_fields JSON, --- 自定义字段
|
||||
page_path VARCHAR(255), --- 文章路径
|
||||
page_status publication_status DEFAULT 'draft' --- 文章状态
|
||||
);
|
||||
--- 文章表
|
||||
@ -48,7 +47,6 @@ CREATE TABLE posts
|
||||
post_status publication_status DEFAULT 'draft', --- 文章状态
|
||||
post_editor BOOLEAN DEFAULT FALSE, --- 文章是否编辑未保存
|
||||
post_unsaved_content TEXT, --- 未保存的文章
|
||||
post_path VARCHAR(255), --- 文章路径
|
||||
post_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 文章创建时间
|
||||
post_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 文章更新时间
|
||||
post_published_at TIMESTAMP, --- 文章发布时间
|
||||
|
@ -79,6 +79,20 @@ async fn install() -> status::Custom<String> {
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/system")]
|
||||
async fn token_system() -> Result<status::Custom<String>, status::Custom<String>> {
|
||||
// 创建 Claims
|
||||
let claims = secret::CustomClaims {
|
||||
user_id: String::from("system"),
|
||||
device_ua: String::from("system"),
|
||||
};
|
||||
// 生成JWT
|
||||
let token = secret::generate_jwt(claims,Duration::seconds(1))
|
||||
.map_err(|e| status::Custom(Status::InternalServerError, format!("JWT generation failed: {}", e)))?;
|
||||
|
||||
Ok(status::Custom(Status::Ok, token))
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动Rocket应用
|
||||
*/
|
||||
@ -88,26 +102,7 @@ async fn rocket() -> _ {
|
||||
init_db(config.db_config)
|
||||
.await
|
||||
.expect("Failed to connect to database"); // 初始化数据库连接
|
||||
rocket::build().mount("/", routes![install, ssql]) // 挂载API路由
|
||||
rocket::build()
|
||||
.mount("/", routes![install, ssql]) // 挂载API路由
|
||||
.mount("/auth/token", routes![token_system])
|
||||
}
|
||||
|
||||
|
||||
// fn main(){
|
||||
// // secret::generate_key().expect("msg");
|
||||
// // 创建claims
|
||||
// let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
|
||||
|
||||
// // 创建 Claims
|
||||
// let claims = secret::CustomClaims {
|
||||
// user_id: String::from("lsy"),
|
||||
// device_ua: String::from("lsy"),
|
||||
// };
|
||||
// let t=String::from("eyJhbGciOiJFZERTQSJ9.eyJleHAiOjE3MzE3NTczMDMsIm5iZiI6MTczMTc1NzI4MywiaWF0IjoxNzMxNzU3MjgzLCJ1c2VyX2lkIjoibHN5IiwiZGV2aWNlX3VhIjoibHN5In0.C8t5XZFSKnnDVmc6WkY-gzGNSAP7lNAjP9yBjhdvIRO7r_QjDnfcm0INIqCt5cyvnRlE2rFJIx_axOfLx2QJAw");
|
||||
// // 生成JWT
|
||||
// let token = secret::generate_jwt(claims,Duration::seconds(20)).expect("msg");
|
||||
// println!("{}", token);
|
||||
|
||||
// // 验证JWT
|
||||
// let a=secret::validate_jwt(&t).expect("msg");
|
||||
// println!("\n\n{}",a.user_id)
|
||||
// }
|
50
frontend/components/ErrorBoundary.tsx
Normal file
50
frontend/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
// File path: components/ErrorBoundary.tsx
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { ThemeService } from '../services/themeService';
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasError: false
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error('Uncaught error:', error, errorInfo);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
const themeService = ThemeService.getInstance();
|
||||
try {
|
||||
// 尝试使用主题的错误模板
|
||||
const errorTemplate = themeService.getTemplate('error');
|
||||
return <div dangerouslySetInnerHTML={{ __html: errorTemplate }} />;
|
||||
} catch (e) {
|
||||
// 如果无法获取主题模板,显示默认错误页面
|
||||
return (
|
||||
<div className="error-page">
|
||||
<h1>Something went wrong</h1>
|
||||
<p>{this.state.error?.message}</p>
|
||||
<button onClick={() => this.setState({ hasError: false })}>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
18
frontend/components/LoadingBoundary.tsx
Normal file
18
frontend/components/LoadingBoundary.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
// File path: components/LoadingBoundary.tsx
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
interface LoadingBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const LoadingBoundary: React.FC<LoadingBoundaryProps> = ({
|
||||
children,
|
||||
fallback = <div>Loading...</div>
|
||||
}) => {
|
||||
return (
|
||||
<Suspense fallback={fallback}>
|
||||
{children}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
@ -7,7 +7,6 @@
|
||||
* 还包括插件的生命周期钩子和依赖项的配置。
|
||||
*/
|
||||
import { SerializeType } from "contracts/generalContract";
|
||||
import { CapabilityProps } from "contracts/capabilityContract";
|
||||
|
||||
|
||||
export interface PluginConfig {
|
||||
@ -20,8 +19,6 @@ export interface PluginConfig {
|
||||
icon?: string; // 插件图标URL(可选)
|
||||
managePath?: string; // 插件管理页面路径(可选)
|
||||
configuration?: PluginConfiguration; // 插件配置
|
||||
/** 声明需要使用的能力,没有实际作用 */
|
||||
capabilities?: Set<CapabilityProps<void>>;
|
||||
routs: Set<{
|
||||
description?: string; // 路由描述(可选)
|
||||
path: string; // 路由路径
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { CapabilityProps } from "contracts/capabilityContract";
|
||||
|
||||
export interface TemplateContract {
|
||||
// 模板名称
|
||||
name: string;
|
||||
@ -14,9 +12,6 @@ export interface TemplateContract {
|
||||
// 模板脚本
|
||||
scripts?: string[];
|
||||
};
|
||||
/** 声明需要使用的能力,没有实际作用 */
|
||||
capabilities?: Set<CapabilityProps<void>>;
|
||||
|
||||
// 渲染函数
|
||||
render: (props: any) => React.ReactNode;
|
||||
}
|
@ -12,10 +12,10 @@ import { SerializeType } from "contracts/generalContract";
|
||||
export interface ThemeConfig {
|
||||
name: string; // 主题的名称
|
||||
displayName: string; // 主题的显示名称
|
||||
icon?: string; // 主题图标URL(可选)
|
||||
version: string; // 主题的版本号
|
||||
description?: string; // 主题的描述信息
|
||||
author?: string; // 主题的作者信息
|
||||
entry: string; // 主题的入口路径
|
||||
templates: Map<string, ThemeTemplate>; // 主题模板的映射表
|
||||
/** 主题全局配置 */
|
||||
globalSettings?: {
|
||||
|
@ -1,17 +1,22 @@
|
||||
import { CapabilityService } from "services/capabilityService";
|
||||
import { ThemeService } from "services/themeService";
|
||||
import { createServiceContext } from "./createServiceContext";
|
||||
import { ApiService } from "services/apiService";
|
||||
import { createServiceContext } from "hooks/createServiceContext";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const { ExtensionProvider, useExtension } = createServiceContext(
|
||||
"Extension",
|
||||
() => CapabilityService.getInstance(),
|
||||
export const { CapabilityProvider, useCapability } = createServiceContext(
|
||||
"Capability", () => CapabilityService.getInstance(),
|
||||
);
|
||||
|
||||
export const { ThemeProvider, useTheme } = createServiceContext("Theme", () =>
|
||||
ThemeService.getInstance(),
|
||||
export const { ThemeProvider, useTheme } = createServiceContext(
|
||||
"Theme", () => ThemeService.getInstance(),
|
||||
);
|
||||
|
||||
export const { ApiProvider, useApi } = createServiceContext(
|
||||
"Api", () => ThemeService.getInstance(),
|
||||
);
|
||||
|
||||
|
||||
// File path:hooks/servicesProvider.tsx
|
||||
/**
|
||||
* ServiceProvider 组件用于提供扩展和主题上下文给其子组件。
|
||||
@ -19,7 +24,11 @@ export const { ThemeProvider, useTheme } = createServiceContext("Theme", () =>
|
||||
* @param children - 要渲染的子组件。
|
||||
*/
|
||||
export const ServiceProvider = ({ children }: { children: ReactNode }) => (
|
||||
<ExtensionProvider>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</ExtensionProvider>
|
||||
<ApiProvider>
|
||||
<CapabilityProvider>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</CapabilityProvider>
|
||||
</ApiProvider>
|
||||
);
|
||||
|
15
frontend/hooks/useAsyncError.tsx
Normal file
15
frontend/hooks/useAsyncError.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
// File path: hooks/useAsyncError.tsx
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useAsyncError() {
|
||||
const [, setError] = useState();
|
||||
|
||||
return useCallback(
|
||||
(e: Error) => {
|
||||
setError(() => {
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
[setError],
|
||||
);
|
||||
}
|
@ -40,26 +40,30 @@ export class ApiService {
|
||||
* @throws Error 如果未找到凭据或请求失败
|
||||
*/
|
||||
private async getSystemToken(): Promise<string> {
|
||||
const credentials = localStorage.getItem('system_credentials');
|
||||
if (!credentials) {
|
||||
throw new Error('System credentials not found');
|
||||
const username = import.meta.env.VITE_SYSTEM_USERNAME;
|
||||
const password = import.meta.env.VITE_SYSTEM_PASSWORD;
|
||||
if (!username || !password ) {
|
||||
throw new Error('Failed to obtain the username or password of the front-end system');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseURL}/auth/system`, {
|
||||
const response = await fetch(`${this.baseURL}/auth/token/system`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(JSON.parse(credentials)),
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get system token');
|
||||
}
|
||||
|
||||
const { token } = await response.json();
|
||||
return token;
|
||||
const data = await response.text();
|
||||
return data; // Assuming the token is in the 'token' field of the response
|
||||
} catch (error) {
|
||||
console.error('Error getting system token:', error);
|
||||
throw error;
|
||||
@ -77,7 +81,7 @@ export class ApiService {
|
||||
public async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
requiresAuth = true
|
||||
auth ?: string
|
||||
): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
@ -85,9 +89,8 @@ export class ApiService {
|
||||
try {
|
||||
const headers = new Headers(options.headers);
|
||||
|
||||
if (requiresAuth) {
|
||||
const token = await this.getSystemToken();
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
if (auth) {
|
||||
headers.set('Authorization', `Bearer ${auth}`);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
||||
|
92
frontend/services/routeManager.ts
Normal file
92
frontend/services/routeManager.ts
Normal file
@ -0,0 +1,92 @@
|
||||
// File path: services/routeManager.ts
|
||||
import { useEffect } from 'react';
|
||||
import { useRoutes, RouteObject } from 'react-router-dom';
|
||||
import { ThemeService } from './themeService';
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
|
||||
export class RouteManager {
|
||||
private static instance: RouteManager;
|
||||
private themeService: ThemeService;
|
||||
private routes: RouteObject[] = [];
|
||||
|
||||
private constructor(themeService: ThemeService) {
|
||||
this.themeService = themeService;
|
||||
}
|
||||
|
||||
public static getInstance(themeService?: ThemeService): RouteManager {
|
||||
if (!RouteManager.instance && themeService) {
|
||||
RouteManager.instance = new RouteManager(themeService);
|
||||
}
|
||||
return RouteManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化路由
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
const themeConfig = this.themeService.getThemeConfig();
|
||||
if (!themeConfig) {
|
||||
throw new Error('Theme configuration not loaded');
|
||||
}
|
||||
|
||||
this.routes = [
|
||||
{
|
||||
path: '/',
|
||||
element: this.createRouteElement(themeConfig.routes.index),
|
||||
errorElement: <ErrorBoundary />,
|
||||
},
|
||||
{
|
||||
path: '/post/:id',
|
||||
element: this.createRouteElement(themeConfig.routes.post),
|
||||
},
|
||||
{
|
||||
path: '/tag/:tag',
|
||||
element: this.createRouteElement(themeConfig.routes.tag),
|
||||
},
|
||||
{
|
||||
path: '/category/:category',
|
||||
element: this.createRouteElement(themeConfig.routes.category),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: this.createRouteElement(themeConfig.routes.error),
|
||||
},
|
||||
];
|
||||
|
||||
// 添加自定义页面路由
|
||||
themeConfig.routes.page.forEach((template, path) => {
|
||||
this.routes.push({
|
||||
path,
|
||||
element: this.createRouteElement(template),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建路由元素
|
||||
*/
|
||||
private createRouteElement(templateName: string) {
|
||||
return (props: any) => {
|
||||
const template = this.themeService.getTemplate(templateName);
|
||||
// 这里可以添加模板渲染逻辑
|
||||
return <div dangerouslySetInnerHTML={{ __html: template }} />;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有路由
|
||||
*/
|
||||
public getRoutes(): RouteObject[] {
|
||||
return this.routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新路由
|
||||
*/
|
||||
public addRoute(path: string, templateName: string): void {
|
||||
this.routes.push({
|
||||
path,
|
||||
element: this.createRouteElement(templateName),
|
||||
});
|
||||
}
|
||||
}
|
@ -1,52 +1,151 @@
|
||||
// service/theme/themeService.ts
|
||||
import { ThemeConfig } from "contracts/themeContract"
|
||||
// File path: services/themeService.ts
|
||||
import { ThemeConfig, ThemeTemplate } from "contracts/themeContract";
|
||||
import { ApiService } from "./apiService";
|
||||
|
||||
export class ThemeService {
|
||||
private static themeInstance: ThemeService; // 单例实例
|
||||
private themeConfig: ThemeConfig | null = null; // 当前主题配置
|
||||
private themeComponents: Map<string, React.ComponentType> = new Map(); // 主题组件缓存
|
||||
private static instance: ThemeService;
|
||||
private currentTheme?: ThemeConfig;
|
||||
private templates: Map<string, string> = new Map();
|
||||
private api: ApiService;
|
||||
|
||||
private constructor() { } // 私有构造函数,防止外部实例化
|
||||
private constructor(api: ApiService) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
// 获取单例实例
|
||||
public static getInstance(): ThemeService {
|
||||
if (!ThemeService.themeInstance) {
|
||||
ThemeService.themeInstance = new ThemeService();
|
||||
public static getInstance(api?: ApiService): ThemeService {
|
||||
if (!ThemeService.instance && api) {
|
||||
ThemeService.instance = new ThemeService(api);
|
||||
}
|
||||
return ThemeService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题服务
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
try {
|
||||
// 从API获取当前主题配置
|
||||
const themeConfig = await this.api.request<ThemeConfig>(
|
||||
'/theme/current',
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
// 加载主题配置
|
||||
await this.loadTheme(themeConfig);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize theme:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题配置
|
||||
*/
|
||||
private async loadTheme(config: ThemeConfig): Promise<void> {
|
||||
try {
|
||||
this.currentTheme = config;
|
||||
await this.loadTemplates();
|
||||
} catch (error) {
|
||||
console.error('Failed to load theme:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题模板
|
||||
*/
|
||||
private async loadTemplates(): Promise<void> {
|
||||
if (!this.currentTheme) {
|
||||
throw new Error('No theme configuration loaded');
|
||||
}
|
||||
|
||||
const loadTemplate = async (template: ThemeTemplate) => {
|
||||
try {
|
||||
const response = await fetch(template.path);
|
||||
const templateContent = await response.text();
|
||||
this.templates.set(template.name, templateContent);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load template ${template.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 并行加载所有模板
|
||||
const loadPromises = Array.from(this.currentTheme.templates.values())
|
||||
.map(template => loadTemplate(template));
|
||||
|
||||
await Promise.all(loadPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题配置
|
||||
*/
|
||||
public getThemeConfig(): ThemeConfig | undefined {
|
||||
return this.currentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板内容
|
||||
*/
|
||||
public getTemplate(templateName: string): string {
|
||||
const template = this.templates.get(templateName);
|
||||
if (!template) {
|
||||
throw new Error(`Template ${templateName} not found`);
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路由获取对应的模板
|
||||
*/
|
||||
public getTemplateByRoute(route: string): string {
|
||||
if (!this.currentTheme) {
|
||||
throw new Error('No theme configuration loaded');
|
||||
}
|
||||
|
||||
let templateName: string | undefined;
|
||||
|
||||
// 检查是否是预定义路由
|
||||
if (route === '/') {
|
||||
templateName = this.currentTheme.routes.index;
|
||||
} else if (route.startsWith('/post/')) {
|
||||
templateName = this.currentTheme.routes.post;
|
||||
} else if (route.startsWith('/tag/')) {
|
||||
templateName = this.currentTheme.routes.tag;
|
||||
} else if (route.startsWith('/category/')) {
|
||||
templateName = this.currentTheme.routes.category;
|
||||
} else {
|
||||
// 检查自定义页面路由
|
||||
templateName = this.currentTheme.routes.page.get(route);
|
||||
}
|
||||
|
||||
if (!templateName) {
|
||||
templateName = this.currentTheme.routes.error;
|
||||
}
|
||||
|
||||
return this.getTemplate(templateName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题配置
|
||||
*/
|
||||
public async updateThemeConfig(config: Partial<ThemeConfig>): Promise<void> {
|
||||
try {
|
||||
const updatedConfig = await this.api.request<ThemeConfig>(
|
||||
'/theme/config',
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
}
|
||||
return ThemeService.themeInstance;
|
||||
}
|
||||
|
||||
// 加载主题
|
||||
async loadTheme(themeConfig: ThemeConfig): Promise<void> {
|
||||
this.themeConfig = themeConfig; // 设置当前主题
|
||||
await this.loadThemeComponents(themeConfig); // 加载主题组件
|
||||
}
|
||||
|
||||
// 加载主题组件
|
||||
private async loadThemeComponents(config:ThemeConfig): Promise<void> {
|
||||
// 清除现有组件缓存
|
||||
this.themeComponents.clear();
|
||||
|
||||
// 动态导入主题入口组件
|
||||
const entryComponent = await import(config.entry);
|
||||
this.themeComponents.set('entry', entryComponent.default); // 缓存入口路径
|
||||
|
||||
// 加载所有模板组件
|
||||
for (const [key, template] of config.templates.entries()) {
|
||||
const component = await import(template.path);
|
||||
this.themeComponents.set(key, component.default); // 缓存模板组件
|
||||
}
|
||||
}
|
||||
|
||||
// 获取指定模板名称的组件
|
||||
getComponent(templateName: string): React.ComponentType | null {
|
||||
return this.themeComponents.get(templateName) || null; // 返回组件或null
|
||||
}
|
||||
|
||||
// 获取当前主题配置
|
||||
getCurrentTheme(): ThemeConfig | null {
|
||||
return this.currentTheme; // 返回当前主题配置
|
||||
);
|
||||
|
||||
await this.loadTheme(updatedConfig);
|
||||
} catch (error) {
|
||||
console.error('Failed to update theme configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ThemeConfig } from "types/themeTypeRequirement";
|
||||
import { ThemeConfig } from "contracts/themeContract";
|
||||
|
||||
export const themeConfig: ThemeConfig = {
|
||||
name: 'default',
|
||||
@ -15,7 +15,6 @@ export const themeConfig: ThemeConfig = {
|
||||
}],
|
||||
]),
|
||||
|
||||
settingsSchema: undefined,
|
||||
routes: {
|
||||
post: "",
|
||||
tag: "",
|
||||
|
Loading…
Reference in New Issue
Block a user