From e4500b2658ba2e1c5794aa5ead412f3b9cbb7d32 Mon Sep 17 00:00:00 2001 From: diogohudson Date: Tue, 10 Mar 2026 11:50:58 -0300 Subject: [PATCH 1/3] fix: add missing Portuguese diacritics/accents in translations Co-Authored-By: Claude Opus 4.6 --- ui/src/i18n/locales/pt.json | 52 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/ui/src/i18n/locales/pt.json b/ui/src/i18n/locales/pt.json index 28527b7..8f805ff 100644 --- a/ui/src/i18n/locales/pt.json +++ b/ui/src/i18n/locales/pt.json @@ -3,61 +3,61 @@ "title": "Only Once Share" }, "hero": { - "title": "Compartilhe segredos com seguranca", - "subtitle": "Criptografia ponta a ponta no seu navegador. O servidor nunca ve seus dados. Links se autodestroem apos uma visualizacao." + "title": "Compartilhe segredos com segurança", + "subtitle": "Criptografia ponta a ponta no seu navegador. O servidor nunca vê seus dados. Links se autodestroem após uma visualização." }, "create": { - "label": "Conteudo secreto", + "label": "Conteúdo secreto", "placeholder": "Cole sua senha, chave de API ou mensagem privada...", "charCount": "{{count}} / 50,000", "expiresIn": "Expira em", "encrypting": "Criptografando...", "submit": "Criar link secreto", "linkCreated": "Link secreto criado", - "linkInfo": "Compartilhe este link com seu destinatario. Ele so pode ser aberto uma vez, depois e permanentemente destruido.", + "linkInfo": "Compartilhe este link com seu destinatário. Ele só pode ser aberto uma vez, depois é permanentemente destruído.", "shareVia": "Compartilhar via", "copy": "Copiar", "copied": "Copiado", "whatsapp": "WhatsApp", "email": "E-mail", "createAnother": "Criar outro", - "whatsappMsg": "Estou compartilhando um segredo com voce. Abra este link para ve-lo (apenas uma vez):\n\n{{link}}", - "emailSubject": "Aqui esta um segredo para voce", - "emailBody": "Estou compartilhando um segredo com voce. Abra este link para ve-lo — so pode ser aberto uma vez:\n\n{{link}}" + "whatsappMsg": "Estou compartilhando um segredo com você. Abra este link para vê-lo (apenas uma vez):\n\n{{link}}", + "emailSubject": "Aqui está um segredo para você", + "emailBody": "Estou compartilhando um segredo com você. Abra este link para vê-lo — só pode ser aberto uma vez:\n\n{{link}}" }, "view": { "loading": "Recuperando e descriptografando segredo...", - "destroyed": "Este segredo foi permanentemente destruido. Nao pode ser visualizado novamente.", + "destroyed": "Este segredo foi permanentemente destruído. Não pode ser visualizado novamente.", "copySecret": "Copiar segredo", - "copiedClipboard": "Copiado para a area de transferencia", - "notFoundTitle": "Segredo nao disponivel", - "notFoundMsg": "Este segredo ja foi visualizado ou expirou. Segredos so podem ser acessados uma vez.", + "copiedClipboard": "Copiado para a área de transferência", + "notFoundTitle": "Segredo não disponível", + "notFoundMsg": "Este segredo já foi visualizado ou expirou. Segredos só podem ser acessados uma vez.", "errorTitle": "Algo deu errado", - "errorMsg": "Nao foi possivel descriptografar o segredo. O link pode ser invalido.", - "invalidLink": "Link invalido — chave de descriptografia ausente", + "errorMsg": "Não foi possível descriptografar o segredo. O link pode ser inválido.", + "invalidLink": "Link inválido — chave de descriptografia ausente", "newSecret": "Compartilhar novo segredo", - "backHome": "Voltar ao inicio" + "backHome": "Voltar ao início" }, "footer": { "encryption": "AES-256-GCM", "zeroKnowledge": "Conhecimento zero", - "autoDelete": "Auto-exclusao" + "autoDelete": "Auto-exclusão" }, "security": { "title": "Como funciona", "e2eTitle": "Criptografia ponta a ponta", - "e2eDesc": "Seu segredo e criptografado no seu navegador usando AES-256-GCM com um IV aleatorio de 96 bits antes de sair do seu dispositivo. A chave de criptografia nunca e enviada ao nosso servidor.", - "hkdfTitle": "Derivacao de chave HKDF", - "hkdfDesc": "Uma chave de criptografia unica e derivada para cada segredo usando HKDF-SHA-256 com o ID do segredo como contexto. Mesmo com a chave mestra, cada segredo tem sua propria chave criptograficamente independente.", - "aadTitle": "Vinculacao de dados autenticados", - "aadDesc": "O ID do segredo e vinculado como Dados Autenticados Adicionais (AAD) durante a criptografia. Se alguem adulterar o ID ou trocar o texto cifrado entre segredos, a descriptografia falhara.", + "e2eDesc": "Seu segredo é criptografado no seu navegador usando AES-256-GCM com um IV aleatório de 96 bits antes de sair do seu dispositivo. A chave de criptografia nunca é enviada ao nosso servidor.", + "hkdfTitle": "Derivação de chave HKDF", + "hkdfDesc": "Uma chave de criptografia única é derivada para cada segredo usando HKDF-SHA-256 com o ID do segredo como contexto. Mesmo com a chave mestra, cada segredo tem sua própria chave criptograficamente independente.", + "aadTitle": "Vinculação de dados autenticados", + "aadDesc": "O ID do segredo é vinculado como Dados Autenticados Adicionais (AAD) durante a criptografia. Se alguém adulterar o ID ou trocar o texto cifrado entre segredos, a descriptografia falhará.", "zkTitle": "Conhecimento zero", - "zkDesc": "O servidor armazena apenas dados criptografados. Nao podemos ler, descriptografar ou acessar seus segredos de nenhuma forma.", + "zkDesc": "O servidor armazena apenas dados criptografados. Não podemos ler, descriptografar ou acessar seus segredos de nenhuma forma.", "keyTitle": "A chave nunca sai do navegador", - "keyDesc": "A chave de descriptografia e colocada apos o # na URL. Fragmentos de URL do navegador nunca sao enviados aos servidores.", - "oneTimeTitle": "Visualizacao unica", - "oneTimeDesc": "Quando um segredo e recuperado, ele e atomicamente excluido do armazenamento na mesma operacao.", - "expiryTitle": "Expiracao automatica", - "expiryDesc": "Segredos expiram automaticamente apos o TTL escolhido (1-72 horas), mesmo se nunca forem visualizados." + "keyDesc": "A chave de descriptografia é colocada após o # na URL. Fragmentos de URL do navegador nunca são enviados aos servidores.", + "oneTimeTitle": "Visualização única", + "oneTimeDesc": "Quando um segredo é recuperado, ele é atomicamente excluído do armazenamento na mesma operação.", + "expiryTitle": "Expiração automática", + "expiryDesc": "Segredos expiram automaticamente após o TTL escolhido (1-72 horas), mesmo se nunca forem visualizados." } } From aecf4400c3ba4bb24743593d1182728e3c09fdcd Mon Sep 17 00:00:00 2001 From: diogohudson Date: Tue, 10 Mar 2026 12:00:36 -0300 Subject: [PATCH 2/3] fix: sanitize PostHog properties to strip encryption keys from URL fragments PostHog was capturing full URLs including #fragment which contains the encryption key. Added sanitize_properties callback to strip fragments from all URL-related properties. Also added .env templates, gitignored .env files, and configured docker-compose env_file for API service. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 ++++ api/.env.template | 1 + docker-compose.yml | 2 ++ ui/.env.template | 1 + ui/src/lib/posthog.ts | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 40 insertions(+) create mode 100644 api/.env.template create mode 100644 ui/.env.template diff --git a/.gitignore b/.gitignore index ea85e57..303c324 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,7 @@ api/.coverage # TypeScript build info *.tsbuildinfo + +# Environment variables (secrets) +.env +!.env.template diff --git a/api/.env.template b/api/.env.template new file mode 100644 index 0000000..2b769ca --- /dev/null +++ b/api/.env.template @@ -0,0 +1 @@ +POSTHOG_API_KEY= diff --git a/docker-compose.yml b/docker-compose.yml index 99df932..78a30ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,8 @@ services: api: build: ./api + env_file: + - ./api/.env environment: - REDIS_HOST=redis - REDIS_PORT=6379 diff --git a/ui/.env.template b/ui/.env.template new file mode 100644 index 0000000..1ccec93 --- /dev/null +++ b/ui/.env.template @@ -0,0 +1 @@ +VITE_POSTHOG_KEY= diff --git a/ui/src/lib/posthog.ts b/ui/src/lib/posthog.ts index dc4f805..98ab58d 100644 --- a/ui/src/lib/posthog.ts +++ b/ui/src/lib/posthog.ts @@ -3,12 +3,44 @@ import posthog from "posthog-js"; const POSTHOG_KEY = import.meta.env.VITE_POSTHOG_KEY; const POSTHOG_HOST = import.meta.env.VITE_POSTHOG_HOST || "https://us.i.posthog.com"; +/** Strip URL fragment (#key) from any string that looks like a URL. */ +function stripFragment(value: unknown): unknown { + if (typeof value === "string" && value.includes("#")) { + return value.split("#")[0]; + } + return value; +} + +/** Properties that may contain the encryption key in the URL fragment. */ +const URL_PROPS = [ + "$current_url", + "$pathname", + "$referrer", + "$initial_current_url", + "$initial_referrer", + "$pageview_id", +]; + if (POSTHOG_KEY) { posthog.init(POSTHOG_KEY, { api_host: POSTHOG_HOST, capture_pageview: true, capture_pageleave: true, autocapture: true, + sanitize_properties(properties, _event) { + for (const key of URL_PROPS) { + if (key in properties) { + properties[key] = stripFragment(properties[key]); + } + } + // Also strip from any property containing a URL with a fragment + for (const [key, value] of Object.entries(properties)) { + if (typeof value === "string" && value.includes("#") && value.includes("/s/")) { + properties[key] = stripFragment(value); + } + } + return properties; + }, }); } From 0c6c4d19b4dccf895b8bd5fccb66bbe4e7bf8f2b Mon Sep 17 00:00:00 2001 From: diogohudson Date: Tue, 10 Mar 2026 12:02:58 -0300 Subject: [PATCH 3/3] fix: flush PostHog events after capture to ensure delivery The Python PostHog SDK batches events asynchronously. Without explicit flush(), events were queued but never sent in the Flask request cycle. Co-Authored-By: Claude Opus 4.6 --- api/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/app.py b/api/app.py index 1de7df8..590e9b4 100644 --- a/api/app.py +++ b/api/app.py @@ -132,6 +132,7 @@ def create_secret(): if posthog: posthog.capture("server", "secret_created", {"ttl_hours": ttl_hours, "has_alias": alias is not None}) + posthog.flush() return jsonify({"id": secret_id, "alias": alias}), 201 @@ -174,6 +175,7 @@ def get_secret(secret_id): if posthog: posthog.capture("server", "secret_retrieved", {"via": "alias" if alias_used else "uuid"}) + posthog.flush() return jsonify({"ciphertext": ciphertext, "id": actual_id})