diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md new file mode 100644 index 00000000..6c9fe982 --- /dev/null +++ b/deploy/kubernetes/README.md @@ -0,0 +1,40 @@ +# Kubernetes deployment + +Reference manifests for running this Enclosed fork (WEIN & CO Notes) on Kubernetes. +See the full guide with explanations in [`docs: Self-hosting → Kubernetes`](../../packages/docs/src/self-hosting/kubernetes.md). + +## Quick start + +```bash +# 1. Build and push the branded image (the branding is baked in at build time) +docker build -t ghcr.io/predator2003/enclosed:v1.16.0-weinco . +docker push ghcr.io/predator2003/enclosed:v1.16.0-weinco + +# 2. Create the secret +cp secret.example.yaml secret.yaml +# edit secret.yaml: set AUTHENTICATION_JWT_SECRET (openssl rand -base64 48) +# then uncomment "- secret.yaml" in kustomization.yaml + +# 3. Adjust notes.example.com in ingress.yaml, then deploy +kubectl apply -k . + +# 4. Check +kubectl -n enclosed get pods +kubectl -n enclosed logs deploy/enclosed +``` + +## Important operational notes + +- **HTTPS is mandatory.** The client-side encryption uses the browser WebCrypto API, + which is only available in secure contexts. Plain HTTP will show a warning and + note creation will not work. +- **Exactly 1 replica.** The default fs-lite storage is a single-writer embedded + store on the PVC. Do not scale the Deployment horizontally. +- **Non-root:** the manifests run the pod as UID/GID 1000 with `fsGroup: 1000`. + `PUID`/`PGID` environment variables are not supported by the image. +- **Upload size:** the ingress `proxy-body-size` must be at least + `NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH` (default 50 MiB) plus headroom. +- **JWT secret:** the server refuses to start when authentication is enabled and + `AUTHENTICATION_JWT_SECRET` is still the default value. +- **Backups:** all notes are encrypted client-side; backing up the PVC never + exposes plaintext. diff --git a/deploy/kubernetes/configmap.yaml b/deploy/kubernetes/configmap.yaml new file mode 100644 index 00000000..855bf5dc --- /dev/null +++ b/deploy/kubernetes/configmap.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: enclosed-config + namespace: enclosed + labels: + app.kubernetes.io/name: enclosed +data: + PORT: "8787" + # TLS is terminated at the ingress; the pod itself speaks plain HTTP. + SERVER_USE_HTTPS: "false" + # Storage path inside the container; the PVC is mounted there. + STORAGE_DRIVER_FS_LITE_PATH: "/app/.data" + # Maximum encrypted payload size in bytes (50 MiB). If you raise this, raise + # the ingress body-size limit as well (see ingress.yaml). + NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH: "52428800" + # Hourly cleanup of expired notes. + TASK_DELETE_EXPIRED_NOTES_ENABLED: "true" + TASK_DELETE_EXPIRED_NOTES_CRON: "0 * * * *" + TASK_DELETE_EXPIRED_NOTES_RUN_ON_STARTUP: "true" + # Default note lifetime (1 hour) and whether "never expires" is allowed. + PUBLIC_DEFAULT_NOTE_TTL_SECONDS: "3600" + PUBLIC_IS_SETTING_NO_EXPIRATION_ALLOWED: "false" + # Set to "true" (and provide AUTHENTICATION_USERS in the secret) to require + # login for creating notes. + PUBLIC_IS_AUTHENTICATION_REQUIRED: "false" diff --git a/deploy/kubernetes/deployment.yaml b/deploy/kubernetes/deployment.yaml new file mode 100644 index 00000000..811a9326 --- /dev/null +++ b/deploy/kubernetes/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: enclosed + namespace: enclosed + labels: + app.kubernetes.io/name: enclosed +spec: + # fs-lite storage on a ReadWriteOnce volume supports exactly one writer: + # keep replicas at 1 and use the Recreate strategy so the old pod releases + # the volume before the new one starts. + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: enclosed + template: + metadata: + labels: + app.kubernetes.io/name: enclosed + spec: + securityContext: + # The rootless image runs as UID/GID 1000; fsGroup makes the mounted + # volume group-writable for it (PUID/PGID env vars are NOT supported). + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: enclosed + # For the WEIN & CO branded build, build and push your own image from + # this repository (see deploy/kubernetes/README.md). The upstream + # image corentinth/enclosed:latest-rootless works but is unbranded. + image: ghcr.io/predator2003/enclosed:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8787 + envFrom: + - configMapRef: + name: enclosed-config + - secretRef: + name: enclosed-secrets + volumeMounts: + - name: data + mountPath: /app/.data + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + readinessProbe: + httpGet: + path: /api/ping + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/ping + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + # Allow headroom for large encrypted payloads (50 MiB default cap). + memory: 512Mi + volumes: + - name: data + persistentVolumeClaim: + claimName: enclosed-data diff --git a/deploy/kubernetes/ingress.yaml b/deploy/kubernetes/ingress.yaml new file mode 100644 index 00000000..584d1e10 --- /dev/null +++ b/deploy/kubernetes/ingress.yaml @@ -0,0 +1,33 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: enclosed + namespace: enclosed + labels: + app.kubernetes.io/name: enclosed + annotations: + # HTTPS is REQUIRED: the client-side encryption uses the WebCrypto API, + # which browsers only expose in secure contexts. + cert-manager.io/cluster-issuer: letsencrypt-prod + # Must be >= NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH (50 MiB default) plus headroom, + # otherwise large notes/files fail at the ingress before reaching the app. + nginx.ingress.kubernetes.io/proxy-body-size: 64m + nginx.ingress.kubernetes.io/proxy-read-timeout: "60" + nginx.ingress.kubernetes.io/proxy-send-timeout: "60" +spec: + ingressClassName: nginx + tls: + - hosts: + - notes.example.com + secretName: enclosed-tls + rules: + - host: notes.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: enclosed + port: + name: http diff --git a/deploy/kubernetes/kustomization.yaml b/deploy/kubernetes/kustomization.yaml new file mode 100644 index 00000000..f3effc06 --- /dev/null +++ b/deploy/kubernetes/kustomization.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - configmap.yaml + # Copy secret.example.yaml to secret.yaml, fill in real values first: + # - secret.yaml + - pvc.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + +# Override the image in one place, e.g.: +# images: +# - name: ghcr.io/predator2003/enclosed +# newTag: v1.16.0-weinco diff --git a/deploy/kubernetes/namespace.yaml b/deploy/kubernetes/namespace.yaml new file mode 100644 index 00000000..5a010092 --- /dev/null +++ b/deploy/kubernetes/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: enclosed + labels: + app.kubernetes.io/name: enclosed diff --git a/deploy/kubernetes/pvc.yaml b/deploy/kubernetes/pvc.yaml new file mode 100644 index 00000000..05741f4b --- /dev/null +++ b/deploy/kubernetes/pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: enclosed-data + namespace: enclosed + labels: + app.kubernetes.io/name: enclosed +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + # storageClassName: your-storage-class diff --git a/deploy/kubernetes/secret.example.yaml b/deploy/kubernetes/secret.example.yaml new file mode 100644 index 00000000..4ad92777 --- /dev/null +++ b/deploy/kubernetes/secret.example.yaml @@ -0,0 +1,20 @@ +# Copy to secret.yaml, fill in real values, then apply. NEVER commit real secrets. +# +# Generate a strong JWT secret: +# openssl rand -base64 48 +# +# Generate the users list (email:bcrypt-hash pairs, comma separated): +# https://docs.enclosed.cc/self-hosting/users-authentication-key-generator +# or: htpasswd -bnBC 10 "" 'your-password' | tr -d ':\n' +apiVersion: v1 +kind: Secret +metadata: + name: enclosed-secrets + namespace: enclosed + labels: + app.kubernetes.io/name: enclosed +type: Opaque +stringData: + AUTHENTICATION_JWT_SECRET: "REPLACE_ME__openssl_rand_base64_48" + # Only needed when PUBLIC_IS_AUTHENTICATION_REQUIRED is "true": + AUTHENTICATION_USERS: "" diff --git a/deploy/kubernetes/service.yaml b/deploy/kubernetes/service.yaml new file mode 100644 index 00000000..7a46739c --- /dev/null +++ b/deploy/kubernetes/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: enclosed + namespace: enclosed + labels: + app.kubernetes.io/name: enclosed +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: enclosed + ports: + - name: http + port: 80 + targetPort: http diff --git a/packages/app-client/index.html b/packages/app-client/index.html index b0a540d3..561ade14 100644 --- a/packages/app-client/index.html +++ b/packages/app-client/index.html @@ -1,64 +1,28 @@ - + - - Enclosed - Send private and secure notes + + WEIN & CO Notes – Vertrauliche Notizen sicher teilen - - - - - - - - - - - - - - - - - - - + + + - - - + + - - - -

Enclosed - Send Private and Secure Notes

-

Enclosed is a secure platform for sending private notes. All notes are end-to-end encrypted with zero knowledge on the server side.

+

WEIN & CO Notes – Vertrauliche Notizen sicher teilen

+

WEIN & CO Notes ist eine sichere Plattform zum Teilen vertraulicher Notizen. Alle Notizen sind Ende-zu-Ende verschlüsselt – der Server kennt den Inhalt nicht.

- +
diff --git a/packages/app-client/package.json b/packages/app-client/package.json index 4f7728ec..07dedceb 100644 --- a/packages/app-client/package.json +++ b/packages/app-client/package.json @@ -30,6 +30,7 @@ "dependencies": { "@corentinth/chisels": "catalog:", "@enclosed/lib": "workspace:*", + "@fontsource/mulish": "^5.2.8", "@kobalte/core": "^0.13.4", "@solid-primitives/i18n": "^2.1.1", "@solid-primitives/storage": "^4.2.1", @@ -38,7 +39,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "jszip": "^3.10.1", - "lodash-es": "^4.17.21", + "lodash-es": "^4.18.1", "solid-js": "^1.9.5", "solid-sonner": "^0.2.8", "tailwind-merge": "^2.5.2", @@ -56,7 +57,7 @@ "tsx": "^4.19.3", "typescript": "catalog:", "unocss": "^0.64.0", - "vite": "^5.0.11", + "vite": "^5.4.21", "vite-plugin-solid": "^2.11.6", "vitest": "catalog:" } diff --git a/packages/app-client/public/android-chrome-192x192.png b/packages/app-client/public/android-chrome-192x192.png deleted file mode 100644 index cd10d726..00000000 Binary files a/packages/app-client/public/android-chrome-192x192.png and /dev/null differ diff --git a/packages/app-client/public/android-chrome-512x512.png b/packages/app-client/public/android-chrome-512x512.png deleted file mode 100644 index ea5674b8..00000000 Binary files a/packages/app-client/public/android-chrome-512x512.png and /dev/null differ diff --git a/packages/app-client/public/apple-touch-icon.png b/packages/app-client/public/apple-touch-icon.png index bcb28fbf..2ac13168 100644 Binary files a/packages/app-client/public/apple-touch-icon.png and b/packages/app-client/public/apple-touch-icon.png differ diff --git a/packages/app-client/public/favicon-16x16.png b/packages/app-client/public/favicon-16x16.png deleted file mode 100644 index e7dccdb6..00000000 Binary files a/packages/app-client/public/favicon-16x16.png and /dev/null differ diff --git a/packages/app-client/public/favicon-32x32.png b/packages/app-client/public/favicon-32x32.png deleted file mode 100644 index 84c4f227..00000000 Binary files a/packages/app-client/public/favicon-32x32.png and /dev/null differ diff --git a/packages/app-client/public/favicon.ico b/packages/app-client/public/favicon.ico index ff93a47c..5420a1dc 100644 Binary files a/packages/app-client/public/favicon.ico and b/packages/app-client/public/favicon.ico differ diff --git a/packages/app-client/public/humans.txt b/packages/app-client/public/humans.txt index e2d90924..90d1c0f8 100644 --- a/packages/app-client/public/humans.txt +++ b/packages/app-client/public/humans.txt @@ -1,4 +1,7 @@ /* TEAM */ -Developer: Corentin Thomasset -Site: https://corentin.tech -Twitter: @cthmsst \ No newline at end of file +Organization: WEIN & CO Handelsgesellschaft m.b.H. +Site: https://www.weinco.at + +/* THANKS */ +Based on Enclosed by Corentin Thomasset (Apache 2.0) +Site: https://github.com/CorentinTh/enclosed diff --git a/packages/app-client/public/icon-196.png b/packages/app-client/public/icon-196.png new file mode 100644 index 00000000..5b56fa98 Binary files /dev/null and b/packages/app-client/public/icon-196.png differ diff --git a/packages/app-client/public/logo.svg b/packages/app-client/public/logo.svg new file mode 100644 index 00000000..5363d90d --- /dev/null +++ b/packages/app-client/public/logo.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/app-client/public/og-image.png b/packages/app-client/public/og-image.png deleted file mode 100644 index 0400fd1f..00000000 Binary files a/packages/app-client/public/og-image.png and /dev/null differ diff --git a/packages/app-client/public/robots.txt b/packages/app-client/public/robots.txt index 4b9fa7d1..f0c117ce 100644 --- a/packages/app-client/public/robots.txt +++ b/packages/app-client/public/robots.txt @@ -1,4 +1,3 @@ User-agent: * Disallow: / Allow: /$ -Allow: /og-image.png diff --git a/packages/app-client/public/site.webmanifest b/packages/app-client/public/site.webmanifest index 45dc8a20..ba9d55f0 100644 --- a/packages/app-client/public/site.webmanifest +++ b/packages/app-client/public/site.webmanifest @@ -1 +1,11 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "WEIN & CO Notes", + "short_name": "WC Notes", + "icons": [ + { "src": "/icon-196.png", "sizes": "196x196", "type": "image/png" }, + { "src": "/logo.svg", "sizes": "any", "type": "image/svg+xml" } + ], + "theme_color": "#BC0936", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/packages/app-client/src/app.css b/packages/app-client/src/app.css index 74a6e6f9..6ec87370 100644 --- a/packages/app-client/src/app.css +++ b/packages/app-client/src/app.css @@ -1,24 +1,34 @@ +/* + * WEIN & CO corporate theme. + * Values are derived from the weinco.at corporate identity: + * brand red #BC0936 (hover #9F082F), warm greys #F5F1F1 / #EEE9E9 / #D9D4D0, + * charcoal text #3C3C3B. (Champagne gold #CFBB79 = hsl(46 47% 64%) is part of + * the corporate palette and can be added as an accent when needed.) + */ :root { --background: 0 0% 100%; - --foreground: 0 0% 3.9%; + --foreground: 60 1% 23%; --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; + --card-foreground: 60 1% 23%; --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; + --popover-foreground: 60 1% 23%; - --primary: 0 0% 9%; + /* WEIN & CO brand red #BC0936 */ + --primary: 345 91% 39%; --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; + /* Warm light grey #F5F1F1 */ + --secondary: 0 17% 95%; + --secondary-foreground: 60 1% 23%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; + --muted: 0 17% 95%; + --muted-foreground: 0 0% 43%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; + /* Warm grey #EEE9E9 */ + --accent: 0 13% 92%; + --accent-foreground: 60 1% 23%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; @@ -26,34 +36,40 @@ --warning: 31 98% 50%; --warning-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; + /* Warm grey border #D9D4D0 */ + --border: 27 11% 83%; + --input: 27 11% 83%; + --ring: 345 91% 39%; - --radius: 0.5rem; + /* The header keeps the brand red in both color modes */ + --brand: 345 91% 39%; + --brand-foreground: 0 0% 100%; + + --radius: 0.375rem; } [data-kb-theme="dark"] { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; + --background: 0 0% 5%; + --foreground: 0 0% 96%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; + --card: 0 0% 7%; + --card-foreground: 0 0% 96%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; + --popover: 0 0% 7%; + --popover-foreground: 0 0% 96%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; + /* Slightly lightened brand red for contrast on dark surfaces */ + --primary: 345 90% 45%; + --primary-foreground: 0 0% 98%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; + --secondary: 0 0% 14%; + --secondary-foreground: 0 0% 96%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; + --muted: 0 0% 14%; + --muted-foreground: 0 0% 64%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; + --accent: 0 0% 16%; + --accent-foreground: 0 0% 96%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; @@ -61,9 +77,9 @@ --warning: 31 98% 50%; --warning-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; + --border: 0 0% 18%; + --input: 0 0% 18%; + --ring: 345 90% 45%; } @@ -72,4 +88,4 @@ } body { @apply bg-background text-foreground; -} \ No newline at end of file +} diff --git a/packages/app-client/src/assets/favicon.ico b/packages/app-client/src/assets/favicon.ico index b836b2bc..5420a1dc 100644 Binary files a/packages/app-client/src/assets/favicon.ico and b/packages/app-client/src/assets/favicon.ico differ diff --git a/packages/app-client/src/index.tsx b/packages/app-client/src/index.tsx index 68412ea6..de78730b 100644 --- a/packages/app-client/src/index.tsx +++ b/packages/app-client/src/index.tsx @@ -8,6 +8,14 @@ import { NoteContextProvider } from './modules/notes/notes.context'; import { Toaster } from './modules/ui/components/sonner'; import { getRoutes } from './routes'; import '@unocss/reset/tailwind.css'; +// Self-hosted brand font (no external font CDN, keeps the CSP on 'self'). +// Weights match actual usage: 300 (font-light), 400/500/600/700 (body, medium, +// semibold, bold). +import '@fontsource/mulish/300.css'; +import '@fontsource/mulish/400.css'; +import '@fontsource/mulish/500.css'; +import '@fontsource/mulish/600.css'; +import '@fontsource/mulish/700.css'; import 'virtual:uno.css'; import './app.css'; diff --git a/packages/app-client/src/locales/ar.json b/packages/app-client/src/locales/ar.json index 8f50fa6e..6cc68cab 100644 --- a/packages/app-client/src/locales/ar.json +++ b/packages/app-client/src/locales/ar.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "إرسال ملاحظات خاصة وآمنة" }, "insecureContextWarning": { @@ -9,7 +9,6 @@ }, "navbar": { "new-note": "ملاحظة جديدة", - "github": "GitHub", "language": "اللغة", "theme": { "theme": "الشكل", @@ -20,16 +19,12 @@ "settings": { "documentation": "التوثيق", "cli": "مغلق CLI", - "support": "الدعم", "report-bug": "الإبلاغ عن خطأ", "logout": "الخروج", "contribute-to-i18n": "المساهمة في الترجمة" } }, "footer": { - "crafted-by": "من صنع", - "source-code": "الكود المصدري متاح على", - "github": "GitHub", "version": "الإصدار" }, "login": { diff --git a/packages/app-client/src/locales/de.json b/packages/app-client/src/locales/de.json index 07261829..e029a320 100644 --- a/packages/app-client/src/locales/de.json +++ b/packages/app-client/src/locales/de.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Private und sichere Notizen versenden" }, "insecureContextWarning": { @@ -9,35 +9,34 @@ }, "navbar": { "new-note": "Neue Notiz", - "github": "GitHub", "language": "Sprache", + "documentation": "Dokumentation", + "change-theme": "Design wechseln", + "menu-icon": "Menü-Symbol", + "change-language": "Sprache wechseln", "theme": { - "theme": "Theme", + "theme": "Design", "light-mode": "Heller Modus", "dark-mode": "Dunkler Modus", "system-mode": "Systemeinstellungen" }, "settings": { "documentation": "Dokumentation", - "cli": "Enclosed CLI", - "support": "Enclosed unterstützen", + "cli": "WEIN & CO Notes CLI", "report-bug": "Einen Fehler melden", "logout": "Abmelden", "contribute-to-i18n": "Zu i18n beitragen" } }, "footer": { - "crafted-by": "Geschaffen von", - "source-code": "Quellcode verfügbar auf", - "github": "GitHub", "version": "Version" }, "login": { - "title": "An Enclosed anmelden", - "description": "Dies ist eine private Instanz von Enclosed. Geben Sie Ihre Anmeldedaten ein, um Notizen erstellen zu können.", + "title": "An WEIN & CO Notes anmelden", + "description": "Dies ist eine private Instanz von WEIN & CO Notes. Geben Sie Ihre Anmeldedaten ein, um Notizen erstellen zu können.", "email": "E-Mail", "password": "Passwort", - "submit": "Login", + "submit": "Anmelden", "errors": { "invalid-credentials": "Ungültige E-Mail oder ungültiges Passwort.", "unknown": "Ein unbekannter Fehler ist aufgetreten. Bitte versuchen Sie es später noch einmal." @@ -53,6 +52,7 @@ "rate-limit": "Sie haben das Limit für die Erstellung von Notizen überschritten. Bitte versuchen Sie es später erneut.", "too-large": "Der Inhalt der Notiz und die Anhänge sind zu groß. Bitte verkleinern Sie die Größe und versuchen Sie es erneut.", "unauthorized": "Sie sind nicht berechtigt, Notizen zu erstellen. Bitte melden Sie sich an und versuchen Sie es erneut.", + "api-not-found": "Der API-Endpunkt wurde nicht gefunden. Der Server ist möglicherweise nicht korrekt konfiguriert.", "unknown": "Beim Erstellen der Notiz ist ein Fehler aufgetreten, bitte versuchen Sie es erneut." }, "share": { @@ -64,9 +64,13 @@ "placeholder": "Schreiben Sie Ihre Notiz hier...", "password": { "label": "Notiz Passwort", - "placeholder": "Passwort..." + "placeholder": "Passwort...", + "hide-password": "Passwort verbergen", + "show-password": "Passwort anzeigen", + "generate-random-password": "Zufälliges Passwort generieren" }, "expiration": "Verfallszeit", + "no-expiration": "Die Notiz läuft nie ab", "delays": { "1h": "1 Stunde", "1d": "1 Tag", @@ -91,13 +95,26 @@ "with-deletion": "Die Notiz wird nach dem Lesen automatisch gelöscht.", "copy-link": "Link kopieren", "copy-success": "Der Link wurde kopiert" + }, + "qr-code": { + "title": "Notiz am Mobilgerät teilen", + "description": "Scannen Sie diesen QR-Code, um die Notiz auf Ihrem Mobilgerät zu öffnen. Sie können den QR-Code auch als Bild exportieren und weitergeben.", + "export": "QR-Code exportieren", + "download-svg": "Als SVG herunterladen", + "download-png": "Als PNG herunterladen", + "download-success": "QR-Code heruntergeladen", + "copy-svg": "SVG-Code kopieren", + "copy-success": "SVG in die Zwischenablage kopiert" } }, "view": { + "loading": "Notiz wird geladen...", "note-content": "Inhalt der Notiz", "assets": { "download": "Herunterladen", - "download-all": "Alle Dateien herunterladen" + "download-all": "Alle Dateien herunterladen", + "heading-multiple": "{{ count }} Dateien an dieser Notiz angehängt", + "heading-single": "1 Datei an dieser Notiz angehängt" }, "request-password": { "description": "Diese Notiz ist passwortgeschützt. Bitte geben Sie das Passwort ein, um sie zu entsperren.", diff --git a/packages/app-client/src/locales/en.json b/packages/app-client/src/locales/en.json index de370e20..4bc88ccc 100644 --- a/packages/app-client/src/locales/en.json +++ b/packages/app-client/src/locales/en.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Send private and secure notes" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "New note", - "github": "GitHub", "language": "Language", - "github-repository": "GitHub repository", "documentation": "Documentation", "change-theme": "Change theme", "menu-icon": "Menu icon", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "Documentation", - "cli": "Enclosed CLI", - "support": "Support Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Report a bug", "logout": "Logout", "contribute-to-i18n": "Contribute to i18n" } }, "footer": { - "crafted-by": "Crafted by", - "source-code": "Source code available on", - "github": "GitHub", "version": "Version" }, "login": { - "title": "Login to Enclosed", - "description": "This is a private instance of Enclosed. Enter your credentials to be able to create notes.", + "title": "Login to WEIN & CO Notes", + "description": "This is a private instance of WEIN & CO Notes. Enter your credentials to be able to create notes.", "email": "Email", "password": "Password", "submit": "Login", diff --git a/packages/app-client/src/locales/es.json b/packages/app-client/src/locales/es.json index 3c00bc62..5ac2a541 100644 --- a/packages/app-client/src/locales/es.json +++ b/packages/app-client/src/locales/es.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Enviar notas privadas y seguras" }, "insecureContextWarning": { @@ -9,7 +9,6 @@ }, "navbar": { "new-note": "Nueva nota", - "github": "GitHub", "theme": { "light-mode": "Modo claro", "dark-mode": "Modo oscuro", @@ -17,22 +16,18 @@ }, "settings": { "documentation": "Documentación", - "cli": "Enclosed CLI", - "support": "Soporte Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Reportar un error", "logout": "Cerrar sesión", "contribute-to-i18n": "Contribuir a i18n" } }, "footer": { - "crafted-by": "Creado por", - "source-code": "Código fuente disponible en", - "github": "GitHub", "version": "Versión" }, "login": { - "title": "Iniciar sesión en Enclosed", - "description": "Esta es una instancia privada de Enclosed. Introduzca sus credenciales para poder crear notas.", + "title": "Iniciar sesión en WEIN & CO Notes", + "description": "Esta es una instancia privada de WEIN & CO Notes. Introduzca sus credenciales para poder crear notas.", "email": "Correo electrónico", "password": "Contraseña", "submit": "Iniciar sesión", diff --git a/packages/app-client/src/locales/fr.json b/packages/app-client/src/locales/fr.json index 94cf4547..039b67a8 100644 --- a/packages/app-client/src/locales/fr.json +++ b/packages/app-client/src/locales/fr.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Envoyez des notes privées et sécurisées" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "Nouvelle note", - "github": "GitHub", "language": "Langue", - "github-repository": "Dépôt GitHub", "documentation": "Documentation", "change-theme": "Changer de thème", "menu-icon": "Icône du menu", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "Documentation", - "cli": "Enclosed CLI", - "support": "Soutenir Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Signaler un bug", "logout": "Se déconnecter", "contribute-to-i18n": "Contribuer à l'i18n" } }, "footer": { - "crafted-by": "Créé par", - "source-code": "Code source disponible sur", - "github": "GitHub", "version": "Version" }, "login": { - "title": "Connexion à Enclosed", - "description": "Ceci est une instance privée de Enclosed. Entrez vos identifiants pour pouvoir créer des notes.", + "title": "Connexion à WEIN & CO Notes", + "description": "Ceci est une instance privée de WEIN & CO Notes. Entrez vos identifiants pour pouvoir créer des notes.", "email": "Email", "password": "Mot de passe", "submit": "Se connecter", @@ -136,7 +130,6 @@ "description": "Cette note sera supprimée après lecture. Une fois affichée, elle sera définitivement supprimée et ne pourra pas être récupérée.", "confirm": "Je comprends, montrer la note" }, - "error": { "invalid-url": { "title": "URL de note invalide", diff --git a/packages/app-client/src/locales/hu.json b/packages/app-client/src/locales/hu.json index 45a8a59b..77489033 100644 --- a/packages/app-client/src/locales/hu.json +++ b/packages/app-client/src/locales/hu.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Küldj privát és biztonságos jegyzeteket" }, "insecureContextWarning": { @@ -9,7 +9,6 @@ }, "navbar": { "new-note": "Új jegyzet", - "github": "GitHub", "language": "Nyelv", "theme": { "theme": "Megjelenés", @@ -19,22 +18,18 @@ }, "settings": { "documentation": "Dokumentáció", - "cli": "Enclosed CLI", - "support": "Támogasd az Enclosed-ot", + "cli": "WEIN & CO Notes CLI", "report-bug": "Hiba bejelentése", "logout": "Kijelentkezés", "contribute-to-i18n": "Közreműködés az i18n-ben" } }, "footer": { - "crafted-by": "Készítette", - "source-code": "Forráskód elérhető a", - "github": "GitHubon", "version": "Verzió" }, "login": { - "title": "Bejelentkezés az Enclosedba", - "description": "Ez az Enclosed egy privát példánya. Add meg hitelesítő adataidat a jegyzetek létrehozásához.", + "title": "Bejelentkezés a WEIN & CO Notesba", + "description": "Ez a WEIN & CO Notes egy privát példánya. Add meg hitelesítő adataidat a jegyzetek létrehozásához.", "email": "Email", "password": "Jelszó", "submit": "Bejelentkezés", @@ -44,7 +39,7 @@ }, "footer": [ "Nincs fiókod?", - "Vedd fel a kapcsolatot az Enclosed privát példányának tulajdonosával." + "Vedd fel a kapcsolatot a WEIN & CO Notes privát példányának tulajdonosával." ] }, "create": { diff --git a/packages/app-client/src/locales/id.json b/packages/app-client/src/locales/id.json index ab6e566a..7bfae9f3 100644 --- a/packages/app-client/src/locales/id.json +++ b/packages/app-client/src/locales/id.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Kirim catatan pribadi dan aman" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "Catatan baru", - "github": "GitHub", "language": "Bahasa", - "github-repository": "Repositori GitHub", "documentation": "Dokumentasi", "change-theme": "Ubah tema", "menu-icon": "Ikon menu", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "Dokumentasi", - "cli": "Enclosed CLI", - "support": "Dukung Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Laporkan bug", "logout": "Keluar", "contribute-to-i18n": "Berkontribusi pada i18n" } }, "footer": { - "crafted-by": "Dibuat oleh", - "source-code": "Kode sumber tersedia di", - "github": "GitHub", "version": "Versi" }, "login": { - "title": "Masuk ke Enclosed", - "description": "Ini adalah instance pribadi Enclosed. Masukkan kredensial Anda untuk dapat membuat catatan.", + "title": "Masuk ke WEIN & CO Notes", + "description": "Ini adalah instance pribadi WEIN & CO Notes. Masukkan kredensial Anda untuk dapat membuat catatan.", "email": "Email", "password": "Kata Sandi", "submit": "Masuk", diff --git a/packages/app-client/src/locales/it.json b/packages/app-client/src/locales/it.json index 79c2f425..d429cc72 100644 --- a/packages/app-client/src/locales/it.json +++ b/packages/app-client/src/locales/it.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Invia note private e sicure" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "Nuova nota", - "github": "GitHub", "language": "Lingua", - "github-repository": "Repository GitHub", "documentation": "Documentazione", "change-theme": "Cambia tema", "menu-icon": "Icona menu", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "Documentazione", - "cli": "Enclosed CLI", - "support": "Sostieni Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Segnala un bug", "logout": "Disconnetti", "contribute-to-i18n": "Contribuisci alla traduzione" } }, "footer": { - "crafted-by": "Creato da", - "source-code": "Codice sorgente disponibile su", - "github": "GitHub", "version": "Versione" }, "login": { - "title": "Accedi a Enclosed", - "description": "Questa è un'istanza privata di Enclosed. Accedi per creare delle note.", + "title": "Accedi a WEIN & CO Notes", + "description": "Questa è un'istanza privata di WEIN & CO Notes. Accedi per creare delle note.", "email": "Email", "password": "Password", "submit": "Accedi", @@ -136,7 +130,6 @@ "description": "Questa nota sarà cancellata dopo la lettura. Una volta visualizzata sarà definitivamente cancellata e non potrà essere recuperata.", "confirm": "Capisco, visualizza la nota" }, - "error": { "invalid-url": { "title": "URL della nota non valida", diff --git a/packages/app-client/src/locales/nl.json b/packages/app-client/src/locales/nl.json index 07a5d8ce..96eb4b2f 100644 --- a/packages/app-client/src/locales/nl.json +++ b/packages/app-client/src/locales/nl.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Privé en beveiligde notities versturen" }, "insecureContextWarning": { @@ -9,7 +9,6 @@ }, "navbar": { "new-note": "Nieuwe notitie", - "github": "GitHub", "language": "Taal", "theme": { "theme": "Thema", @@ -20,21 +19,17 @@ "settings": { "documentation": "Documentatie", "cli": "Bijgesloten CLI", - "support": "Ondersteun Enclosed", "report-bug": "Meld een bug", "logout": "Uitloggen", "contribute-to-i18n": "Bijdragen aan i18n" } }, "footer": { - "crafted-by": "Gemaakt door", - "source-code": "Broncode beschikbaar op", - "github": "GitHub", "version": "Versie" }, "login": { - "title": "Inloggen op Enclosed", - "description": "Dit is een privé-instantie van Enclosed. Voer je gegevens in om notities te kunnen maken.", + "title": "Inloggen op WEIN & CO Notes", + "description": "Dit is een privé-instantie van WEIN & CO Notes. Voer je gegevens in om notities te kunnen maken.", "email": "Email", "password": "Wachtwoord", "submit": "Inloggen", diff --git a/packages/app-client/src/locales/pl.json b/packages/app-client/src/locales/pl.json index 5250e69c..7918d098 100644 --- a/packages/app-client/src/locales/pl.json +++ b/packages/app-client/src/locales/pl.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Wysyłaj prywatne i bezpieczne notatki" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "Nowa notatka", - "github": "GitHub", "language": "Język", - "github-repository": "Repozytorium GitHub", "documentation": "Dokumentacja", "change-theme": "Zmień motyw", "menu-icon": "Ikona menu", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "Dokumentacja", - "cli": "Enclosed CLI", - "support": "Wspieraj Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Zgłoś błąd", "logout": "Wyloguj", "contribute-to-i18n": "Współtwórz tłumaczenie" } }, "footer": { - "crafted-by": "Stworzone przez", - "source-code": "Kod źródłowy dostępny na", - "github": "GitHub", "version": "Wersja" }, "login": { - "title": "Zaloguj się do Enclosed", - "description": "To jest prywatna instancja Enclosed. Wprowadź swoje dane, aby móc tworzyć notatki.", + "title": "Zaloguj się do WEIN & CO Notes", + "description": "To jest prywatna instancja WEIN & CO Notes. Wprowadź swoje dane, aby móc tworzyć notatki.", "email": "E-mail", "password": "Hasło", "submit": "Zaloguj się", diff --git a/packages/app-client/src/locales/pt-BR.json b/packages/app-client/src/locales/pt-BR.json index c7f589da..d7bc2312 100644 --- a/packages/app-client/src/locales/pt-BR.json +++ b/packages/app-client/src/locales/pt-BR.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Envie notas de forma privada e segura" }, "insecureContextWarning": { @@ -9,7 +9,6 @@ }, "navbar": { "new-note": "Nova nota", - "github": "GitHub", "language": "Idioma", "theme": { "theme": "Aparência", @@ -19,22 +18,18 @@ }, "settings": { "documentation": "Documentação", - "cli": "Enclosed CLI", - "support": "Ajudar Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Reportar erro", "logout": "Sair", "contribute-to-i18n": "Contribuir para i18n" } }, "footer": { - "crafted-by": "Criado por", - "source-code": "Código disponível no", - "github": "GitHub", "version": "Versão" }, "login": { - "title": "Entrar no Enclosed", - "description": "Esta é uma instância privada do Enclosed. Insira os dados da sua conta para poder criar notas.", + "title": "Entrar no WEIN & CO Notes", + "description": "Esta é uma instância privada do WEIN & CO Notes. Insira os dados da sua conta para poder criar notas.", "email": "E-mail", "password": "Senha", "submit": "Entrar", diff --git a/packages/app-client/src/locales/pt.json b/packages/app-client/src/locales/pt.json index dcf9ffdc..e9b185bb 100644 --- a/packages/app-client/src/locales/pt.json +++ b/packages/app-client/src/locales/pt.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Envia notas de forma privada e segura" }, "insecureContextWarning": { @@ -9,7 +9,6 @@ }, "navbar": { "new-note": "Nova nota", - "github": "GitHub", "language": "Linguagem", "theme": { "theme": "Tema", @@ -19,22 +18,18 @@ }, "settings": { "documentation": "Documentação", - "cli": "Enclosed CLI", - "support": "Ajudar Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Reportar erro", "logout": "Terminar sessão", "contribute-to-i18n": "Contribuir para i18n" } }, "footer": { - "crafted-by": "Criado por", - "source-code": "Código disponível no", - "github": "GitHub", "version": "Versão" }, "login": { - "title": "Iniciar sessão no Enclosed", - "description": "Esta é uma instância privada do Enclosed. Insere as tuas credenciais para poderes criar notas.", + "title": "Iniciar sessão no WEIN & CO Notes", + "description": "Esta é uma instância privada do WEIN & CO Notes. Insere as tuas credenciais para poderes criar notas.", "email": "Email", "password": "Palavra-passe", "submit": "Iniciar sessão", @@ -42,7 +37,10 @@ "invalid-credentials": "Email ou palavra-passe inválidos.", "unknown": "Ocorreu um erro. Tenta outra vez mais tarde." }, - "footer": ["Não tens conta?", "Contacta o criador da instância."] + "footer": [ + "Não tens conta?", + "Contacta o criador da instância." + ] }, "create": { "errors": { diff --git a/packages/app-client/src/locales/ru.json b/packages/app-client/src/locales/ru.json index b70b9680..4a838fb1 100644 --- a/packages/app-client/src/locales/ru.json +++ b/packages/app-client/src/locales/ru.json @@ -1,141 +1,136 @@ -{ - "app": { - "title": "Enclosed", - "description": "Отправляйте приватные и защищенные заметки" - }, - "insecureContextWarning": { - "description": "Ваше соединение небезопасно, для правильной работы приложения используйте протокол HTTPS. Вы не сможете создавать или просматривать заметки.", - "learn-more": "Узнать больше" - }, - "navbar": { - "new-note": "Новая заметка", - "github": "GitHub", - "theme": { - "light-mode": "Светлая тема", - "dark-mode": "Тёмная тема", - "system-mode": "Система" - }, - "settings": { - "documentation": "Документация", - "cli": "Enclosed CLI", - "support": "Поддержать Enclosed", - "report-bug": "Сообщить об ошибке", - "logout": "Выйти", - "contribute-to-i18n": "Внести свой вклад в i18n" - } - }, - "footer": { - "crafted-by": "Разработано", - "source-code": "Исходный код здесь:", - "github": "GitHub", - "version": "Версия" - }, - "login": { - "title": "Войти в Enclosed", - "description": "Это приватный режим Enclosed. Введите учётные данные, чтобы создать заметку.", - "email": "Электронная почта", - "password": "Пароль", - "submit": "Логин", - "errors": { - "invalid-credentials": "Неверный адрес электронной почты или пароль.", - "unknown": "Неизвестная ошибка. Пожалуйста, повторите попытку позже." - }, - "footer": [ - "Нет учётной записи?", - "Связаться с владельцем" - ] - }, - "create": { - "errors": { - "empty-note": "Введите содержание заметки или приложите файл", - "rate-limit": "Вы превысили лимит создания заметок. Пожалуйста, повторите попытку позже.", - "too-large": "Превышен объём содержания заметки и вложения. Уменьшите размер и повторите попытку.", - "unauthorized": "У вас нет прав для создания заметки. Войдите в систему и повторите попытку.", - "unknown": "При создании заметки произошла ошибка. Повторите попытку." - }, - "share": { - "button": "Поделиться", - "title": "Полученная заметка", - "description": "Делюсь своей заметкой." - }, - "settings": { - "placeholder": "Введите текст...", - "password": { - "label": "Пароль заметки", - "placeholder": "Пароль..." - }, - "expiration": "Срок истечения", - "delays": { - "1h": "1 час", - "1d": "1 день", - "1w": "1 неделя", - "1m": "1 месяц" - }, - "delete-after-reading": { - "label": "Удалить после прочтения", - "description": "Удалить заметку после прочтения" - }, - "attach-files": "Прикрепить файлы", - "drop-files": { - "title": "Перетащите файлы сюда", - "description": "Перетащите файлы, чтобы прикрепить их к заметке" - }, - "create": "Создать заметку", - "creating": "Создание заметки..." - }, - "success": { - "title": "Заметка успешно создана", - "description": "Заметка создана. Поделиться ", - "with-deletion": "Заметка будет удалена после прочтения.", - "copy-link": "Скопировать ссылку", - "copy-success": "Ссылка скопирована" - } - }, - "view": { - "note-content": "Содержание заметки", - "assets": { - "download": "Скачать", - "download-all": "Загрузить все файлы" - }, - "request-password": { - "description": "Заметка защищена паролем. Введите пароль, чтобы открыть ее.", - "form": { - "label": "Пароль", - "placeholder": "Пароль заметки...", - "unlock-button": "Открыть заметку", - "invalid": "Неверный пароль или URL-адрес заметки." - } - }, - "error": { - "invalid-url": { - "title": "Неверный URL-адрес заметки", - "description": "Ссылка недействительна. Введите корректный URL-адрес." - }, - "rate-limit": { - "title": "Превышен лимит", - "description": "Вы превысили лимит отправки заметок. Пожалуйста, повторите попытку позже." - }, - "unauthorized": { - "title": "Нет доступа", - "description": "Приватная заметка. Войдите в систему, чтобы просмотреть.", - "button": "Вход" - }, - "note-not-found": { - "title": "Заметка не найдена", - "description": "Заметка не существует, срок ее действия истек или она была удалена." - }, - "fetch-error": { - "title": "Ошибка", - "description": "При отправке заметки произошла ошибка. Пожалуйста, повторите попытку позже." - }, - "decryption": { - "title": "Ошибка", - "description": "При расшифровке заметки произошла ошибка. Неверный URL-адрес" - } - } - }, - "copy": { - "label": "Скопировать в буфер обмена", - "success": "Скопировано" - } -} +{ + "app": { + "title": "WEIN & CO Notes", + "description": "Отправляйте приватные и защищенные заметки" + }, + "insecureContextWarning": { + "description": "Ваше соединение небезопасно, для правильной работы приложения используйте протокол HTTPS. Вы не сможете создавать или просматривать заметки.", + "learn-more": "Узнать больше" + }, + "navbar": { + "new-note": "Новая заметка", + "theme": { + "light-mode": "Светлая тема", + "dark-mode": "Тёмная тема", + "system-mode": "Система" + }, + "settings": { + "documentation": "Документация", + "cli": "WEIN & CO Notes CLI", + "report-bug": "Сообщить об ошибке", + "logout": "Выйти", + "contribute-to-i18n": "Внести свой вклад в i18n" + } + }, + "footer": { + "version": "Версия" + }, + "login": { + "title": "Войти в WEIN & CO Notes", + "description": "Это приватный режим WEIN & CO Notes. Введите учётные данные, чтобы создать заметку.", + "email": "Электронная почта", + "password": "Пароль", + "submit": "Логин", + "errors": { + "invalid-credentials": "Неверный адрес электронной почты или пароль.", + "unknown": "Неизвестная ошибка. Пожалуйста, повторите попытку позже." + }, + "footer": [ + "Нет учётной записи?", + "Связаться с владельцем" + ] + }, + "create": { + "errors": { + "empty-note": "Введите содержание заметки или приложите файл", + "rate-limit": "Вы превысили лимит создания заметок. Пожалуйста, повторите попытку позже.", + "too-large": "Превышен объём содержания заметки и вложения. Уменьшите размер и повторите попытку.", + "unauthorized": "У вас нет прав для создания заметки. Войдите в систему и повторите попытку.", + "unknown": "При создании заметки произошла ошибка. Повторите попытку." + }, + "share": { + "button": "Поделиться", + "title": "Полученная заметка", + "description": "Делюсь своей заметкой." + }, + "settings": { + "placeholder": "Введите текст...", + "password": { + "label": "Пароль заметки", + "placeholder": "Пароль..." + }, + "expiration": "Срок истечения", + "delays": { + "1h": "1 час", + "1d": "1 день", + "1w": "1 неделя", + "1m": "1 месяц" + }, + "delete-after-reading": { + "label": "Удалить после прочтения", + "description": "Удалить заметку после прочтения" + }, + "attach-files": "Прикрепить файлы", + "drop-files": { + "title": "Перетащите файлы сюда", + "description": "Перетащите файлы, чтобы прикрепить их к заметке" + }, + "create": "Создать заметку", + "creating": "Создание заметки..." + }, + "success": { + "title": "Заметка успешно создана", + "description": "Заметка создана. Поделиться ", + "with-deletion": "Заметка будет удалена после прочтения.", + "copy-link": "Скопировать ссылку", + "copy-success": "Ссылка скопирована" + } + }, + "view": { + "note-content": "Содержание заметки", + "assets": { + "download": "Скачать", + "download-all": "Загрузить все файлы" + }, + "request-password": { + "description": "Заметка защищена паролем. Введите пароль, чтобы открыть ее.", + "form": { + "label": "Пароль", + "placeholder": "Пароль заметки...", + "unlock-button": "Открыть заметку", + "invalid": "Неверный пароль или URL-адрес заметки." + } + }, + "error": { + "invalid-url": { + "title": "Неверный URL-адрес заметки", + "description": "Ссылка недействительна. Введите корректный URL-адрес." + }, + "rate-limit": { + "title": "Превышен лимит", + "description": "Вы превысили лимит отправки заметок. Пожалуйста, повторите попытку позже." + }, + "unauthorized": { + "title": "Нет доступа", + "description": "Приватная заметка. Войдите в систему, чтобы просмотреть.", + "button": "Вход" + }, + "note-not-found": { + "title": "Заметка не найдена", + "description": "Заметка не существует, срок ее действия истек или она была удалена." + }, + "fetch-error": { + "title": "Ошибка", + "description": "При отправке заметки произошла ошибка. Пожалуйста, повторите попытку позже." + }, + "decryption": { + "title": "Ошибка", + "description": "При расшифровке заметки произошла ошибка. Неверный URL-адрес" + } + } + }, + "copy": { + "label": "Скопировать в буфер обмена", + "success": "Скопировано" + } +} diff --git a/packages/app-client/src/locales/tr.json b/packages/app-client/src/locales/tr.json index fd83ca4a..fefe67dc 100644 --- a/packages/app-client/src/locales/tr.json +++ b/packages/app-client/src/locales/tr.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Gizli ve güvenli notlar gönderin" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "Yeni not", - "github": "GitHub", "language": "Dil", - "github-repository": "GitHub deposu", "documentation": "Dokümantasyon", "change-theme": "Temayı değiştir", "menu-icon": "Menü simgesi", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "Dokümantasyon", - "cli": "Enclosed CLI", - "support": "Enclosed’a destek olun", + "cli": "WEIN & CO Notes CLI", "report-bug": "Hata bildir", "logout": "Çıkış yap", "contribute-to-i18n": "i18n’ye katkıda bulunun" } }, "footer": { - "crafted-by": "Hazırlayan", - "source-code": "Kaynak kodu", - "github": "GitHub", "version": "Sürüm" }, "login": { - "title": "Enclosed’a giriş yapın", - "description": "Bu, Enclosed’un özel bir sürümüdür. Not oluşturabilmek için kimlik bilgilerinizi girin.", + "title": "WEIN & CO Notes’a giriş yapın", + "description": "Bu, WEIN & CO Notes’un özel bir sürümüdür. Not oluşturabilmek için kimlik bilgilerinizi girin.", "email": "E-posta", "password": "Şifre", "submit": "Giriş yap", @@ -47,7 +41,10 @@ "invalid-credentials": "Geçersiz e-posta veya şifre.", "unknown": "Bilinmeyen bir hata oluştu. Lütfen daha sonra tekrar deneyin." }, - "footer": ["Hesabınız yok mu?", "Bu sistemin sahibine ulaşın."] + "footer": [ + "Hesabınız yok mu?", + "Bu sistemin sahibine ulaşın." + ] }, "create": { "errors": { diff --git a/packages/app-client/src/locales/vi.json b/packages/app-client/src/locales/vi.json index 8a38010d..e31789a6 100644 --- a/packages/app-client/src/locales/vi.json +++ b/packages/app-client/src/locales/vi.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "Gửi ghi chú riêng tư và bảo mật" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "Ghi chú mới", - "github": "GitHub", "language": "Ngôn ngữ", - "github-repository": "Kho lưu trữ GitHub", "documentation": "Tài liệu", "change-theme": "Thay đổi giao diện", "menu-icon": "Biểu tượng menu", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "Tài liệu", - "cli": "Enclosed CLI", - "support": "Hỗ trợ Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "Báo cáo lỗi", "logout": "Đăng xuất", "contribute-to-i18n": "Đóng góp dịch thuật" } }, "footer": { - "crafted-by": "Được tạo bởi", - "source-code": "Mã nguồn có sẵn trên", - "github": "GitHub", "version": "Phiên bản" }, "login": { - "title": "Đăng nhập Enclosed", - "description": "Đây là một phiên bản riêng tư của Enclosed. Nhập thông tin đăng nhập của bạn để có thể tạo ghi chú.", + "title": "Đăng nhập WEIN & CO Notes", + "description": "Đây là một phiên bản riêng tư của WEIN & CO Notes. Nhập thông tin đăng nhập của bạn để có thể tạo ghi chú.", "email": "Email", "password": "Mật khẩu", "submit": "Đăng nhập", diff --git a/packages/app-client/src/locales/zh-CN.json b/packages/app-client/src/locales/zh-CN.json index 714fd5bb..3141130f 100644 --- a/packages/app-client/src/locales/zh-CN.json +++ b/packages/app-client/src/locales/zh-CN.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "发送私密、安全的笔记" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "新建笔记", - "github": "GitHub", "language": "语言", - "github-repository": "GitHub 仓库", "documentation": "文档", "change-theme": "更改主题", "menu-icon": "菜单图标", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "文档", - "cli": "Enclosed CLI", - "support": "支持 Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "报告错误", "logout": "登出", "contribute-to-i18n": "贡献翻译" } }, "footer": { - "crafted-by": "作者:", - "source-code": "源代码:", - "github": "GitHub", "version": "版本" }, "login": { - "title": "登录 Enclosed", - "description": "这是一个私有的 Enclosed 实例,输入您的凭据以创建笔记。", + "title": "登录 WEIN & CO Notes", + "description": "这是一个私有的 WEIN & CO Notes 实例,输入您的凭据以创建笔记。", "email": "电子邮箱", "password": "密码", "submit": "登录", @@ -47,7 +41,10 @@ "invalid-credentials": "无效的电子邮箱或密码。", "unknown": "发生未知错误,稍后再试。" }, - "footer": ["没有账户?", "联系实例的拥有者。"] + "footer": [ + "没有账户?", + "联系实例的拥有者。" + ] }, "create": { "errors": { diff --git a/packages/app-client/src/locales/zh-TW.json b/packages/app-client/src/locales/zh-TW.json index 125bcd7d..332672d5 100644 --- a/packages/app-client/src/locales/zh-TW.json +++ b/packages/app-client/src/locales/zh-TW.json @@ -1,6 +1,6 @@ { "app": { - "title": "Enclosed", + "title": "WEIN & CO Notes", "description": "傳送私密且安全的筆記" }, "insecureContextWarning": { @@ -9,9 +9,7 @@ }, "navbar": { "new-note": "新筆記", - "github": "GitHub", "language": "語言", - "github-repository": "GitHub 倉庫", "documentation": "文件", "change-theme": "更改主題", "menu-icon": "選單圖示", @@ -24,22 +22,18 @@ }, "settings": { "documentation": "文件", - "cli": "Enclosed CLI", - "support": "支持 Enclosed", + "cli": "WEIN & CO Notes CLI", "report-bug": "報告錯誤", "logout": "登出", "contribute-to-i18n": "貢獻國際化語言" } }, "footer": { - "crafted-by": "由", - "source-code": "源碼可在", - "github": "GitHub", "version": "版本" }, "login": { - "title": "登入 Enclosed", - "description": "這是一個私人實例的 Enclosed。請輸入您的憑證以創建筆記。", + "title": "登入 WEIN & CO Notes", + "description": "這是一個私人實例的 WEIN & CO Notes。請輸入您的憑證以創建筆記。", "email": "電子郵件", "password": "密碼", "submit": "登入", diff --git a/packages/app-client/src/modules/config/config.provider.tsx b/packages/app-client/src/modules/config/config.provider.tsx index f637b899..bdfb856f 100644 --- a/packages/app-client/src/modules/config/config.provider.tsx +++ b/packages/app-client/src/modules/config/config.provider.tsx @@ -1,13 +1,27 @@ import type { Config } from './config.types'; -import { get } from 'lodash-es'; +import { safelySync } from '@corentinth/chisels'; import { buildTimeConfig } from './config.constants'; export { getConfig, }; +function getRuntimeConfig(): Partial { + // The server injects the runtime config as a non-executable JSON script block, + // which keeps the CSP free of 'unsafe-inline' for scripts. + const configElement = document.getElementById('enclosed-config'); + + if (!configElement?.textContent) { + return {}; + } + + const [runtimeConfig] = safelySync(() => JSON.parse(configElement.textContent!)); + + return runtimeConfig ?? {}; +} + function getConfig(): Config { - const runtimeConfig: Partial = get(window, '__CONFIG__', {}); + const runtimeConfig: Partial = getRuntimeConfig(); const config: Config = { ...buildTimeConfig, diff --git a/packages/app-client/src/modules/notes/components/file-uploader.tsx b/packages/app-client/src/modules/notes/components/file-uploader.tsx index ac4d606b..16f6aa29 100644 --- a/packages/app-client/src/modules/notes/components/file-uploader.tsx +++ b/packages/app-client/src/modules/notes/components/file-uploader.tsx @@ -83,6 +83,10 @@ export const FileUploaderButton: ParentComponent<{ const files = Array.from(target.files); + // Clear the input so selecting the same file again (e.g. after removing it + // from the list) re-triggers the change event. + target.value = ''; + uploadFiles({ files }); }; diff --git a/packages/app-client/src/modules/notes/notes.services.ts b/packages/app-client/src/modules/notes/notes.services.ts index 5e2acd74..3050b138 100644 --- a/packages/app-client/src/modules/notes/notes.services.ts +++ b/packages/app-client/src/modules/notes/notes.services.ts @@ -1,6 +1,6 @@ import { apiClient } from '../shared/http/http-client'; -export { fetchNoteById, fetchNoteExists, storeNote }; +export { confirmNoteRead, fetchNoteById, fetchNoteExists, storeNote }; async function storeNote({ payload, @@ -48,6 +48,18 @@ async function fetchNoteById({ noteId }: { noteId: string }) { return { note }; } +async function confirmNoteRead({ noteId }: { noteId: string }) { + const { deleted } = await apiClient<{ deleted: boolean }>({ + path: `/api/notes/${noteId}/read-confirmation`, + method: 'POST', + // The reader may close the tab right after the note is displayed; keepalive + // lets the deletion request complete anyway. + keepalive: true, + }); + + return { deleted }; +} + async function fetchNoteExists({ noteId }: { noteId: string }) { const { noteExists } = await apiClient<{ noteExists: boolean }>({ method: 'GET', diff --git a/packages/app-client/src/modules/notes/pages/view-note.page.tsx b/packages/app-client/src/modules/notes/pages/view-note.page.tsx index 8f6a70b1..6ec68630 100644 --- a/packages/app-client/src/modules/notes/pages/view-note.page.tsx +++ b/packages/app-client/src/modules/notes/pages/view-note.page.tsx @@ -13,7 +13,7 @@ import { decryptNote, noteAssetsToFiles, parseNoteUrlHashFragment } from '@enclo import { useLocation, useNavigate, useParams } from '@solidjs/router'; import JSZip from 'jszip'; import { type Component, createSignal, type JSX, Match, onMount, Show, Switch } from 'solid-js'; -import { fetchNoteById, fetchNoteExists } from '../notes.services'; +import { confirmNoteRead, fetchNoteById, fetchNoteExists } from '../notes.services'; const RequestPasswordForm: Component<{ onPasswordEntered: (args: { password: string }) => void; getIsPasswordInvalid: () => boolean; setIsPasswordInvalid: (value: boolean) => void }> = (props) => { const [getPassword, setPassword] = createSignal(''); @@ -77,6 +77,7 @@ export const ViewNotePage: Component = () => { const [getEncryptionKey, setEncryptionKey] = createSignal(''); const [getIsPasswordProtected, setIsPasswordProtected] = createSignal(false); + const [getIsDeletedAfterReading, setIsDeletedAfterReading] = createSignal(false); const { t } = useI18n(); const navigate = useNavigate(); @@ -124,6 +125,13 @@ export const ViewNotePage: Component = () => { setFileAssets(files); setDecryptedNote(note.content); setIsPasswordEntered(true); + + if (getIsDeletedAfterReading()) { + // The note is only deleted server-side once decryption succeeded, so a wrong + // password or a link prefetch cannot destroy it. Failures are ignored: the + // reader already has the content, and the note still expires with its TTL. + void safely(confirmNoteRead({ noteId: params.noteId })); + } }; onMount(async () => { @@ -139,6 +147,8 @@ export const ViewNotePage: Component = () => { const { encryptionKey, isPasswordProtected, isDeletedAfterReading } = parsedHashFragment; + setIsDeletedAfterReading(isDeletedAfterReading); + if (isDeletedAfterReading) { const [noteExistsResult, noteExistsError] = await safely(fetchNoteExists({ noteId: params.noteId })); diff --git a/packages/app-client/src/modules/shared/http/http-client.ts b/packages/app-client/src/modules/shared/http/http-client.ts index 54183213..392cb2cb 100644 --- a/packages/app-client/src/modules/shared/http/http-client.ts +++ b/packages/app-client/src/modules/shared/http/http-client.ts @@ -4,7 +4,7 @@ import { buildUrl, getBody } from './http-client.models'; export { apiClient }; -async function apiClient({ path, method, body }: { path: string; method: string; body?: unknown }): Promise { +async function apiClient({ path, method, body, keepalive }: { path: string; method: string; body?: unknown; keepalive?: boolean }): Promise { const config = getConfig(); const url = buildUrl({ path, baseUrl: config.baseApiUrl }); @@ -12,6 +12,7 @@ async function apiClient({ path, method, body }: { path: string; method: stri const response = await fetch(url, { method, + keepalive, headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), diff --git a/packages/app-client/src/modules/ui/layouts/app.layout.tsx b/packages/app-client/src/modules/ui/layouts/app.layout.tsx index f2a8a022..87ddac00 100644 --- a/packages/app-client/src/modules/ui/layouts/app.layout.tsx +++ b/packages/app-client/src/modules/ui/layouts/app.layout.tsx @@ -3,13 +3,11 @@ import { buildTimeConfig } from '@/modules/config/config.constants'; import { getConfig } from '@/modules/config/config.provider'; import { buildDocUrl } from '@/modules/docs/docs.models'; import { useI18n } from '@/modules/i18n/i18n.provider'; -import { useNoteContext } from '@/modules/notes/notes.context'; import { cn } from '@/modules/shared/style/cn'; import { useThemeStore } from '@/modules/theme/theme.store'; import { Button } from '@/modules/ui/components/button'; import { DropdownMenu } from '@kobalte/core/dropdown-menu'; -import { A, useNavigate } from '@solidjs/router'; import { type Component, type ParentComponent, Show } from 'solid-js'; import { DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../components/dropdown-menu'; @@ -61,7 +59,7 @@ const LanguageSwitcher: Component = () => { - + {t('navbar.settings.contribute-to-i18n')} @@ -69,31 +67,37 @@ const LanguageSwitcher: Component = () => { ); }; +// Header icon buttons sit on the brand-red bar and need white icons in both color modes +const brandBarIconTriggerClass = 'text-lg px-0 size-9 text-[hsl(var(--brand-foreground))] hover:bg-white/15 hover:text-[hsl(var(--brand-foreground))]'; + export const Navbar: Component = () => { const themeStore = useThemeStore(); - const { triggerResetNoteForm } = useNoteContext(); - const navigate = useNavigate(); const { t } = useI18n(); const config = getConfig(); const newNoteClicked = () => { - triggerResetNoteForm(); - navigate('/'); + // Full document navigation instead of client-side routing so an authenticating + // reverse proxy (Authelia, oauth2-proxy, ...) in front of the instance gets a + // request and can challenge the user before the create-note UI loads. The full + // reload also resets the note form, so no explicit reset is needed. + window.location.assign('/'); }; // Only show the "New Note" button if the user is authenticated or if authentication is not required const getShouldShowNewNoteButton = () => config.isAuthenticationRequired ? authStore.getIsAuthenticated() : true; return ( -
-
-
- -
@@ -107,12 +111,8 @@ export const Navbar: Component = () => { )} - - - -