diff --git a/packages/backend/src/prisma/migrations/20240522182248_add_legal_columns/migration.sql b/packages/backend/src/prisma/migrations/20240522182248_add_legal_columns/migration.sql new file mode 100644 index 000000000..c7c8bfa2d --- /dev/null +++ b/packages/backend/src/prisma/migrations/20240522182248_add_legal_columns/migration.sql @@ -0,0 +1,50 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_settings" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "rateLimitWindow" INTEGER NOT NULL, + "rateLimitMax" INTEGER NOT NULL, + "secret" TEXT NOT NULL, + "serviceName" TEXT NOT NULL, + "chunkSize" TEXT NOT NULL, + "chunkedUploadsTimeout" INTEGER NOT NULL, + "maxSize" TEXT NOT NULL, + "generateZips" BOOLEAN NOT NULL, + "generateOriginalFileNameWithIdentifier" BOOLEAN NOT NULL DEFAULT false, + "generatedFilenameLength" INTEGER NOT NULL, + "generatedAlbumLength" INTEGER NOT NULL, + "generatedLinksLength" INTEGER NOT NULL DEFAULT 8, + "blockedExtensions" TEXT NOT NULL, + "blockNoExtension" BOOLEAN NOT NULL, + "publicMode" BOOLEAN NOT NULL, + "userAccounts" BOOLEAN NOT NULL, + "disableStatisticsCron" BOOLEAN NOT NULL, + "disableUpdateCheck" BOOLEAN NOT NULL DEFAULT false, + "backgroundImageURL" TEXT NOT NULL, + "logoURL" TEXT NOT NULL, + "metaDescription" TEXT NOT NULL, + "metaKeywords" TEXT NOT NULL, + "metaTwitterHandle" TEXT NOT NULL, + "metaDomain" TEXT NOT NULL DEFAULT '', + "serveUploadsFrom" TEXT NOT NULL DEFAULT '', + "enableMixedCaseFilenames" BOOLEAN NOT NULL DEFAULT true, + "usersStorageQuota" TEXT NOT NULL DEFAULT '0', + "useNetworkStorage" BOOLEAN NOT NULL DEFAULT false, + "useMinimalHomepage" BOOLEAN NOT NULL DEFAULT false, + "useUrlShortener" BOOLEAN NOT NULL DEFAULT false, + "generateThumbnails" BOOLEAN NOT NULL DEFAULT true, + "privacyPolicyPageContent" TEXT NOT NULL DEFAULT '', + "termsOfServicePageContent" TEXT NOT NULL DEFAULT '', + "rulesPageContent" TEXT NOT NULL DEFAULT '', + "S3Region" TEXT NOT NULL DEFAULT '', + "S3Bucket" TEXT NOT NULL DEFAULT '', + "S3AccessKey" TEXT NOT NULL DEFAULT '', + "S3SecretKey" TEXT NOT NULL DEFAULT '', + "S3Endpoint" TEXT NOT NULL DEFAULT '', + "S3PublicUrl" TEXT NOT NULL DEFAULT '' +); +INSERT INTO "new_settings" ("S3AccessKey", "S3Bucket", "S3Endpoint", "S3PublicUrl", "S3Region", "S3SecretKey", "backgroundImageURL", "blockNoExtension", "blockedExtensions", "chunkSize", "chunkedUploadsTimeout", "disableStatisticsCron", "disableUpdateCheck", "enableMixedCaseFilenames", "generateOriginalFileNameWithIdentifier", "generateThumbnails", "generateZips", "generatedAlbumLength", "generatedFilenameLength", "generatedLinksLength", "id", "logoURL", "maxSize", "metaDescription", "metaDomain", "metaKeywords", "metaTwitterHandle", "publicMode", "rateLimitMax", "rateLimitWindow", "secret", "serveUploadsFrom", "serviceName", "useMinimalHomepage", "useNetworkStorage", "useUrlShortener", "userAccounts", "usersStorageQuota") SELECT "S3AccessKey", "S3Bucket", "S3Endpoint", "S3PublicUrl", "S3Region", "S3SecretKey", "backgroundImageURL", "blockNoExtension", "blockedExtensions", "chunkSize", "chunkedUploadsTimeout", "disableStatisticsCron", "disableUpdateCheck", "enableMixedCaseFilenames", "generateOriginalFileNameWithIdentifier", "generateThumbnails", "generateZips", "generatedAlbumLength", "generatedFilenameLength", "generatedLinksLength", "id", "logoURL", "maxSize", "metaDescription", "metaDomain", "metaKeywords", "metaTwitterHandle", "publicMode", "rateLimitMax", "rateLimitWindow", "secret", "serveUploadsFrom", "serviceName", "useMinimalHomepage", "useNetworkStorage", "useUrlShortener", "userAccounts", "usersStorageQuota" FROM "settings"; +DROP TABLE "settings"; +ALTER TABLE "new_settings" RENAME TO "settings"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/packages/backend/src/prisma/schema.prisma b/packages/backend/src/prisma/schema.prisma index 0c028da92..104a4297f 100644 --- a/packages/backend/src/prisma/schema.prisma +++ b/packages/backend/src/prisma/schema.prisma @@ -152,6 +152,9 @@ model settings { useMinimalHomepage Boolean @default(false) useUrlShortener Boolean @default(false) generateThumbnails Boolean @default(true) + privacyPolicyPageContent String @default("") + termsOfServicePageContent String @default("") + rulesPageContent String @default("") S3Region String @default("") S3Bucket String @default("") S3AccessKey String @default("") diff --git a/packages/backend/src/routes/GetSetting.ts b/packages/backend/src/routes/GetSetting.ts new file mode 100644 index 000000000..40f7a0f88 --- /dev/null +++ b/packages/backend/src/routes/GetSetting.ts @@ -0,0 +1,68 @@ +import type { FastifyReply } from 'fastify'; +import { z } from 'zod'; +import type { RequestWithUser } from '@/structures/interfaces.js'; +import { SETTINGS } from '@/structures/settings.js'; + +const publicSettings = [ + 'serviceName', + 'metaDescription', + 'metaKeywords', + 'metaTwitterHandle', + 'metaDomain', + 'chunkSize', + 'maxSize', + 'logoURL', + 'backgroundImageURL', + 'publicMode', + 'userAccounts', + 'blockedExtensions', + 'useNetworkStorage', + 'useMinimalHomepage', + 'serveUploadsFrom', + 'useUrlShortener', + 'privacyPolicyPageContent', + 'termsOfServicePageContent', + 'rulesPageContent' +]; + +export const schema = { + summary: 'Get setting', + describe: 'Get the current value of a specific setting of the instance', + tags: ['Server'], + response: { + 200: z.object({ + value: z.any().optional().describe('The value of the requested setting key.') + }) + } +}; + +export const options = { + url: '/settings/:key', + method: 'get', + middlewares: [ + { + name: 'auth', + optional: true + }, + { + name: 'apiKey', + optional: true + } + ] +}; + +export const run = (req: RequestWithUser, res: FastifyReply) => { + const { key } = req.params as { key: string }; + + if (publicSettings.includes(key)) { + return res.send({ value: SETTINGS[key] }); + } + + if (!req.user) return res.status(403).send({ message: 'Forbidden' }); + + if (!req.user.roles.some(role => role.name === 'admin' || role.name === 'owner')) + return res.status(403).send({ message: 'Forbidden' }); + + if (!SETTINGS[key]) return res.status(404).send({ message: 'Setting not found' }); + return res.send({ value: SETTINGS[key] }); +}; diff --git a/packages/backend/src/routes/GetSettings.ts b/packages/backend/src/routes/GetSettings.ts index 631e31eea..1801b31ae 100644 --- a/packages/backend/src/routes/GetSettings.ts +++ b/packages/backend/src/routes/GetSettings.ts @@ -27,7 +27,16 @@ export const schema = { .optional() .describe('Whether or not to use a minimal version of the homepage.'), useUrlShortener: z.boolean().describe('Whether or not to use the URL shortener.'), - serveUploadsFrom: z.string().optional().describe('The URL to serve uploads from.') + serveUploadsFrom: z.string().optional().describe('The URL to serve uploads from.'), + privacyPolicyPageContent: z + .boolean() + .optional() + .describe('Whether or not the privacy policy page is enabled.'), + termsOfServicePageContent: z + .boolean() + .optional() + .describe('Whether or not the terms of service page is enabled.'), + rulesPageContent: z.boolean().optional().describe('Whether or not the rules page is enabled.') }) } }; @@ -54,6 +63,9 @@ export const run = (_: RequestWithUser, res: FastifyReply) => { useNetworkStorage: SETTINGS.useNetworkStorage, useMinimalHomepage: SETTINGS.useMinimalHomepage, serveUploadsFrom: SETTINGS.serveUploadsFrom, - useUrlShortener: SETTINGS.useUrlShortener + useUrlShortener: SETTINGS.useUrlShortener, + privacyPolicyPageContent: Boolean(SETTINGS.privacyPolicyPageContent), + termsOfServicePageContent: Boolean(SETTINGS.termsOfServicePageContent), + rulesPageContent: Boolean(SETTINGS.rulesPageContent) }); }; diff --git a/packages/backend/src/structures/interfaces.ts b/packages/backend/src/structures/interfaces.ts index d417bd155..f00ad60ec 100644 --- a/packages/backend/src/structures/interfaces.ts +++ b/packages/backend/src/structures/interfaces.ts @@ -134,13 +134,16 @@ export interface Settings { metaKeywords: string; metaTwitterHandle: string; port: number; + privacyPolicyPageContent: string; publicMode: boolean; rateLimitMax: number; rateLimitWindow: number; + rulesPageContent: string; secret: string; serveUploadsFrom: string; serviceName: string; statisticsCron: string; + termsOfServicePageContent: string; updateCheckCron: string; useNetworkStorage: boolean; useUrlShortener: boolean; diff --git a/packages/backend/src/structures/settings.ts b/packages/backend/src/structures/settings.ts index 21d705ade..345c26e12 100644 --- a/packages/backend/src/structures/settings.ts +++ b/packages/backend/src/structures/settings.ts @@ -74,6 +74,9 @@ export const loadSettings = async (force = false) => { SETTINGS.S3SecretKey = settingsTable.S3SecretKey; SETTINGS.S3Endpoint = settingsTable.S3Endpoint; SETTINGS.S3PublicUrl = settingsTable.S3PublicUrl; + SETTINGS.privacyPolicyPageContent = settingsTable.privacyPolicyPageContent; + SETTINGS.termsOfServicePageContent = settingsTable.termsOfServicePageContent; + SETTINGS.rulesPageContent = settingsTable.rulesPageContent; return; } @@ -116,7 +119,10 @@ export const loadSettings = async (force = false) => { S3AccessKey: '', S3SecretKey: '', S3Endpoint: '', - S3PublicUrl: '' + S3PublicUrl: '', + privacyPolicyPageContent: '', + termsOfServicePageContent: '', + rulesPageContent: '' }; await prisma.settings.create({ @@ -390,5 +396,23 @@ const SETTINGS_META = { description: 'Whether or not to use a minimal version of the homepage.', name: 'Use Minimal Homepage', category: 'customization' + }, + privacyPolicyPageContent: { + type: 'text', + description: 'The markdown content for the privacy policy page. Leave empty to disable.', + name: 'Privacy Policy Page', + category: 'legal' + }, + termsOfServicePageContent: { + type: 'text', + description: 'The markdown content for the terms of service page. Leave empty to disable.', + name: 'Terms of Service Page', + category: 'legal' + }, + rulesPageContent: { + type: 'text', + description: 'The markdown content for the rules page. Leave empty to disable.', + name: 'Rules Page', + category: 'legal' } }; diff --git a/packages/next/public/meta-album.jpg b/packages/next/public/meta-album.jpg deleted file mode 100644 index 2629c2c50..000000000 Binary files a/packages/next/public/meta-album.jpg and /dev/null differ diff --git a/packages/next/public/meta-faq.jpg b/packages/next/public/meta-faq.jpg deleted file mode 100644 index fcb2804db..000000000 Binary files a/packages/next/public/meta-faq.jpg and /dev/null differ diff --git a/packages/next/public/meta-guides.jpg b/packages/next/public/meta-guides.jpg deleted file mode 100644 index 48c733539..000000000 Binary files a/packages/next/public/meta-guides.jpg and /dev/null differ diff --git a/packages/next/public/meta-nsfw-album.jpg b/packages/next/public/meta-nsfw-album.jpg deleted file mode 100644 index 9e5bfae7b..000000000 Binary files a/packages/next/public/meta-nsfw-album.jpg and /dev/null differ diff --git a/packages/next/public/meta-snippet.jpg b/packages/next/public/meta-snippet.jpg deleted file mode 100644 index 24f828d16..000000000 Binary files a/packages/next/public/meta-snippet.jpg and /dev/null differ diff --git a/packages/next/public/meta.jpg b/packages/next/public/meta.jpg deleted file mode 100644 index aaa916e02..000000000 Binary files a/packages/next/public/meta.jpg and /dev/null differ diff --git a/packages/next/src/app/(home)/(legal)/privacy-policy/page.tsx b/packages/next/src/app/(home)/(legal)/privacy-policy/page.tsx new file mode 100644 index 000000000..adf97e159 --- /dev/null +++ b/packages/next/src/app/(home)/(legal)/privacy-policy/page.tsx @@ -0,0 +1,46 @@ +import { CustomMDX } from '@/components/mdx/Mdx'; +import request from '@/lib/request'; +import { notFound } from 'next/navigation'; + +export function generateMetadata() { + const section = 'Privacy policy'; + return { + title: section, + description: section, + openGraph: { + title: section, + description: section, + type: 'article', + images: ['/og'] + }, + twitter: { + card: 'summary_large_image', + title: section, + description: section, + images: ['/og'] + } + }; +} + +export default async function PrivacyPolicy() { + const { data } = await request.get({ + url: `settings/privacyPolicyPageContent`, + options: { + next: { + tags: ['settings'] + } + } + }); + + if (!data) { + return notFound(); + } + + return ( +
+
+ +
+
+ ); +} diff --git a/packages/next/src/app/(home)/(legal)/rules/page.tsx b/packages/next/src/app/(home)/(legal)/rules/page.tsx new file mode 100644 index 000000000..97380d944 --- /dev/null +++ b/packages/next/src/app/(home)/(legal)/rules/page.tsx @@ -0,0 +1,46 @@ +import { CustomMDX } from '@/components/mdx/Mdx'; +import request from '@/lib/request'; +import { notFound } from 'next/navigation'; + +export function generateMetadata() { + const section = 'Rules'; + return { + title: section, + description: section, + openGraph: { + title: section, + description: section, + type: 'article', + images: ['/og'] + }, + twitter: { + card: 'summary_large_image', + title: section, + description: section, + images: ['/og'] + } + }; +} + +export default async function Rules() { + const { data } = await request.get({ + url: `settings/rulesPageContent`, + options: { + next: { + tags: ['settings'] + } + } + }); + + if (!data) { + return notFound(); + } + + return ( +
+
+ +
+
+ ); +} diff --git a/packages/next/src/app/(home)/(legal)/terms-of-service/page.tsx b/packages/next/src/app/(home)/(legal)/terms-of-service/page.tsx new file mode 100644 index 000000000..a5b56c2ef --- /dev/null +++ b/packages/next/src/app/(home)/(legal)/terms-of-service/page.tsx @@ -0,0 +1,46 @@ +import { CustomMDX } from '@/components/mdx/Mdx'; +import request from '@/lib/request'; +import { notFound } from 'next/navigation'; + +export function generateMetadata() { + const section = 'Terms of service'; + return { + title: section, + description: section, + openGraph: { + title: section, + description: section, + type: 'article', + images: ['/og'] + }, + twitter: { + card: 'summary_large_image', + title: section, + description: section, + images: ['/og'] + } + }; +} + +export default async function TermsOfService() { + const { data } = await request.get({ + url: `settings/termsOfServicePageContent`, + options: { + next: { + tags: ['settings'] + } + } + }); + + if (!data) { + return notFound(); + } + + return ( +
+
+ +
+
+ ); +} diff --git a/packages/next/src/app/(home)/faq/page.tsx b/packages/next/src/app/(home)/faq/page.tsx index e3e705402..cbb4e738c 100644 --- a/packages/next/src/app/(home)/faq/page.tsx +++ b/packages/next/src/app/(home)/faq/page.tsx @@ -55,7 +55,7 @@ export default function Faq() { '@type': 'BlogPosting', headline: post.metadata.title, description: post.metadata.summary, - image: post.metadata.image ? post.metadata.image : '/meta.jpg' + image: post.metadata.image ? post.metadata.image : '/og?section=faq' }) }} /> diff --git a/packages/next/src/app/(home)/guides/[slug]/page.tsx b/packages/next/src/app/(home)/guides/[slug]/page.tsx index ec9851414..296107d1e 100644 --- a/packages/next/src/app/(home)/guides/[slug]/page.tsx +++ b/packages/next/src/app/(home)/guides/[slug]/page.tsx @@ -55,7 +55,7 @@ export default function Guide({ params }: { readonly params: { slug: string } }) '@type': 'BlogPosting', headline: post.metadata.title, description: post.metadata.summary, - image: post.metadata.image ? post.metadata.image : '/meta.jpg' + image: post.metadata.image ? post.metadata.image : '/og?section=faq' }) }} /> diff --git a/packages/next/src/app/dashboard/admin/settings/page.tsx b/packages/next/src/app/dashboard/admin/settings/page.tsx index 433c449cc..fff5a5fbc 100644 --- a/packages/next/src/app/dashboard/admin/settings/page.tsx +++ b/packages/next/src/app/dashboard/admin/settings/page.tsx @@ -51,7 +51,8 @@ export default async function DashboardAdminSettingsServicePage() { uploads: [] as Setting[], users: [] as Setting[], other: [] as Setting[], - customization: [] as Setting[] + customization: [] as Setting[], + legal: [] as Setting[] }; for (const setting of response.settings) { diff --git a/packages/next/src/components/Footer.tsx b/packages/next/src/components/Footer.tsx index 89a8d8ee5..a2d2f0567 100644 --- a/packages/next/src/components/Footer.tsx +++ b/packages/next/src/components/Footer.tsx @@ -1,8 +1,18 @@ import { cn } from '@/lib/utils'; import { ChibisafeDefaultLogo } from '@/components/svg/ChibisafeLogo'; import Link from 'next/link'; +import request from '@/lib/request'; + +export const SiteFooter = async ({ className = '' }: { readonly className?: string }) => { + const { data } = await request.get({ + url: 'settings', + options: { + next: { + tags: ['settings'] + } + } + }); -export function SiteFooter({ className = '' }: { readonly className?: string }) { return ( ); -} +}; diff --git a/packages/next/src/components/dialogs/CreateShortUrlDialog.tsx b/packages/next/src/components/dialogs/CreateShortUrlDialog.tsx index baa9f0ec1..afe5b509a 100644 --- a/packages/next/src/components/dialogs/CreateShortUrlDialog.tsx +++ b/packages/next/src/components/dialogs/CreateShortUrlDialog.tsx @@ -29,7 +29,6 @@ export function CreateShortUrlDialog({ className }: { readonly className?: strin }); useEffect(() => { - console.log(state); if (state.type === MessageType.Error) toast.error(state.message); else if (state.type === MessageType.Success) { toast.success(state.message); diff --git a/packages/next/src/components/forms/FormWrapper.tsx b/packages/next/src/components/forms/FormWrapper.tsx index 283a64270..f8eaf53ac 100644 --- a/packages/next/src/components/forms/FormWrapper.tsx +++ b/packages/next/src/components/forms/FormWrapper.tsx @@ -5,6 +5,7 @@ import type { PropsWithChildren } from 'react'; import { FormFieldNumber } from './fields/FormFieldNumber'; import { FormFieldBoolean } from './fields/FormFieldBoolean'; import { FormFieldString } from './fields/FormFieldString'; +import { FormFieldText } from './fields/FormFieldText'; export const FormWrapper = ({ form, meta }: PropsWithChildren<{ readonly form: any; readonly meta: Setting[] }>) => { return ( @@ -17,6 +18,8 @@ export const FormWrapper = ({ form, meta }: PropsWithChildren<{ readonly form: a return ; case 'boolean': return ; + case 'text': + return ; default: return
No component found for type: {setting.type}
; } diff --git a/packages/next/src/components/forms/SettingsForm.tsx b/packages/next/src/components/forms/SettingsForm.tsx index abc35ed20..79f13e9a7 100644 --- a/packages/next/src/components/forms/SettingsForm.tsx +++ b/packages/next/src/components/forms/SettingsForm.tsx @@ -65,7 +65,11 @@ const formSchema = z.object({ generateZips: z.boolean().optional(), generatedAlbumLength: z.coerce.number(), generatedLinksLength: z.coerce.number(), - useUrlShortener: z.boolean().optional() + useUrlShortener: z.boolean().optional(), + // Legal + privacyPolicyPage: z.string().optional(), + termsOfServicePage: z.string().optional(), + rulesPage: z.string().optional() }); type FormValues = z.infer; @@ -123,6 +127,7 @@ export const SettingsForm = ({ Users Other Customization + Legal {form.formState.errors ? ( @@ -164,6 +169,9 @@ export const SettingsForm = ({ + + + diff --git a/packages/next/src/components/forms/fields/FormFieldText.tsx b/packages/next/src/components/forms/fields/FormFieldText.tsx new file mode 100644 index 000000000..7fd28b0d6 --- /dev/null +++ b/packages/next/src/components/forms/fields/FormFieldText.tsx @@ -0,0 +1,30 @@ +import type { Setting } from '@/types'; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../../ui/form'; +import { Textarea } from '@/components/ui/textarea'; + +export const FormFieldText = ({ form, data }: { readonly data: Setting; readonly form: any }) => { + return ( + ( + + {data.name} + +