Version: 1759353996237
FurryPlace Payment succeeded!
Thank you for your support!
Go to map
diff --git a/frontend-backup/site.webmanifest b/frontend-backup/site.webmanifest
index 2611d92..b303156 100644
--- a/frontend-backup/site.webmanifest
+++ b/frontend-backup/site.webmanifest
@@ -1,7 +1,7 @@
{
- "name": "openplace",
- "short_name": "openplace",
- "description": "openplace is a free unofficial open source backend for wplace.",
+ "name": "FurryPlace",
+ "short_name": "FurryPlace",
+ "description": "FurryPlace is a free unofficial open source backend for wplace.",
"start_url": "/",
"theme_color": "#f8f4f0",
"background_color": "#ffffff",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index ad6281f..d9f7835 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -191,3 +191,15 @@ model City {
updatedAt DateTime @updatedAt @default(now())
regions Region[]
}
+
+model Rule {
+ id Int @id @default(autoincrement())
+ text String @db.Text
+ locale String @default("en")
+ order Int
+ enabled Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@index([locale, enabled, order])
+}
diff --git a/src/routes/rules.ts b/src/routes/rules.ts
new file mode 100644
index 0000000..1d04401
--- /dev/null
+++ b/src/routes/rules.ts
@@ -0,0 +1,261 @@
+import type { App, Response, NextFunction } from '@tinyhttp/app';
+import { prisma } from '../config/database.js';
+import { authMiddleware } from '../middleware/auth.js';
+import { AuthenticatedRequest, UserRole } from '../types/index.js';
+
+const adminMiddleware = async (req: AuthenticatedRequest, res: Response, next?: NextFunction) => {
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: req.user!.id }
+ });
+ if (!user || user.role !== UserRole.Admin) {
+ return res.status(403).json({ error: 'Forbidden', status: 403 });
+ }
+ return next?.();
+ } catch (error) {
+ console.error('Error fetching user:', error);
+ return res.status(500).json({ error: 'Internal Server Error', status: 500 });
+ }
+};
+
+export function setupRulesRoutes(app: App) {
+ // Public endpoint - Get rules for display
+ app.get('/api/rules', async (req, res) => {
+ try {
+ const locale = (req.query['locale'] as string) || 'en';
+
+ const rules = await prisma.rule.findMany({
+ where: {
+ locale: locale,
+ enabled: true,
+ },
+ orderBy: {
+ order: 'asc',
+ },
+ select: {
+ id: true,
+ text: true,
+ order: true,
+ },
+ });
+
+ return res.json({ rules });
+ } catch (error) {
+ console.error('Error fetching rules:', error);
+ return res.status(500).json({ error: 'Failed to fetch rules', status: 500 });
+ }
+ });
+
+ // Admin endpoint - Get all rules for editing
+ app.get(
+ '/api/admin/rules',
+ authMiddleware,
+ adminMiddleware,
+ async (_req, res) => {
+ try {
+ const rules = await prisma.rule.findMany({
+ orderBy: [{ locale: 'asc' }, { order: 'asc' }],
+ });
+
+ return res.json({ rules });
+ } catch (error) {
+ console.error('Error fetching rules:', error);
+ return res.status(500).json({ error: 'Failed to fetch rules', status: 500 });
+ }
+ }
+ );
+
+ // Admin endpoint - Create or update a rule
+ app.post(
+ '/api/admin/rules',
+ authMiddleware,
+ adminMiddleware,
+ async (req: AuthenticatedRequest, res) => {
+ try {
+ const { id, text, locale = 'en', order, enabled = true } = req.body;
+
+ if (!text || order === undefined) {
+ return res.status(400).json({ error: 'Text and order are required', status: 400 });
+ }
+
+ let rule;
+ if (id) {
+ // Update existing rule
+ rule = await prisma.rule.update({
+ where: { id },
+ data: {
+ text,
+ locale,
+ order,
+ enabled,
+ },
+ });
+ } else {
+ // Create new rule
+ rule = await prisma.rule.create({
+ data: {
+ text,
+ locale,
+ order,
+ enabled,
+ },
+ });
+ }
+
+ return res.json({ rule });
+ } catch (error) {
+ console.error('Error saving rule:', error);
+ return res.status(500).json({ error: 'Failed to save rule', status: 500 });
+ }
+ }
+ );
+
+ // Admin endpoint - Delete a rule
+ app.delete(
+ '/api/admin/rules/:id',
+ authMiddleware,
+ adminMiddleware,
+ async (req, res) => {
+ try {
+ const id = parseInt(req.params['id'] || '');
+ if (isNaN(id)) {
+ return res.status(400).json({ error: 'Invalid rule ID', status: 400 });
+ }
+
+ await prisma.rule.delete({
+ where: { id },
+ });
+
+ return res.json({ success: true });
+ } catch (error) {
+ console.error('Error deleting rule:', error);
+ return res.status(500).json({ error: 'Failed to delete rule', status: 500 });
+ }
+ }
+ );
+
+ // Admin endpoint - Initialize default rules
+ app.post(
+ '/api/admin/rules/initialize',
+ authMiddleware,
+ adminMiddleware,
+ async (_req, res) => {
+ try {
+ const defaultRules = [
+ // English rules
+ {
+ text: '📜 All users are responsible for the content they post. The platform reserves the right of final interpretation.',
+ locale: 'en',
+ order: 0,
+ enabled: true,
+ },
+ {
+ text: '🛑 Any violation may result in immediate removal of content and permanent ban of the account',
+ locale: 'en',
+ order: 1,
+ enabled: true,
+ },
+ {
+ text: '😈 Do not paint over other artworks using random colors or patterns just to mess things up',
+ locale: 'en',
+ order: 2,
+ enabled: true,
+ },
+ {
+ text: "🙅 Disclosing other's personal information is not allowed",
+ locale: 'en',
+ order: 3,
+ enabled: true,
+ },
+ {
+ text: '✅ NSFW content is allowed to a reasonable extent(No Cub, Loli, Guro, Bathroom fetishes, ...)',
+ locale: 'en',
+ order: 4,
+ enabled: true,
+ },
+ {
+ text: '🧑🤝🧑 Do not paint with more than one account',
+ locale: 'en',
+ order: 5,
+ enabled: true,
+ },
+ {
+ text: '✅ Painting over other artworks to complement them or create a new drawing is allowed',
+ locale: 'en',
+ order: 6,
+ enabled: true,
+ },
+ {
+ text: '✅ Griefing political party flags or portraits of politicians is allowed',
+ locale: 'en',
+ order: 7,
+ enabled: true,
+ },
+
+ // Portuguese rules
+ {
+ text: '📜 Todos os utilizadores são responsáveis pelo conteúdo que publicam. A plataforma reserva-se o direito de interpretação final.',
+ locale: 'pt',
+ order: 0,
+ enabled: true,
+ },
+ {
+ text: '🛑 Qualquer violação pode resultar na remoção imediata do conteúdo e banimento permanente da conta',
+ locale: 'pt',
+ order: 1,
+ enabled: true,
+ },
+ {
+ text: '😈 Não desenhe sobre outras obras usando cores ou padrões aleatórios apenas para estragar as coisas',
+ locale: 'pt',
+ order: 2,
+ enabled: true,
+ },
+ {
+ text: '🙅 A divulgação de informações pessoais de terceiros não é permitida',
+ locale: 'pt',
+ order: 3,
+ enabled: true,
+ },
+ {
+ text: '✅ Conteúdo NSFW é permitido até certo ponto razoável (Sem Cub, Loli, Guro, fetiches de banheiro, ...)',
+ locale: 'pt',
+ order: 4,
+ enabled: true,
+ },
+ {
+ text: '🧑🤝🧑 Não desenhe com mais de uma conta',
+ locale: 'pt',
+ order: 5,
+ enabled: true,
+ },
+ {
+ text: '✅ Desenhar sobre outras artes para complementar ou criar novas artes é permitido',
+ locale: 'pt',
+ order: 6,
+ enabled: true,
+ },
+ {
+ text: '✅ Desenhar sobre bandeiras de partidos e retratos de políticos é permitido',
+ locale: 'pt',
+ order: 7,
+ enabled: true,
+ },
+ ];
+
+ const results = await Promise.all(
+ defaultRules.map((rule) =>
+ prisma.rule.create({
+ data: rule,
+ })
+ )
+ );
+
+ return res.json({ initialized: results.length, rules: results });
+ } catch (error) {
+ console.error('Error initializing rules:', error);
+ return res.status(500).json({ error: 'Failed to initialize rules', status: 500 });
+ }
+ }
+ );
+}