From 6ca7952c13614dce86875bc23bf643d84d63e1c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 10:55:13 +0000 Subject: [PATCH 1/2] fix: Telefonnummern normalisieren (keine Duplikat-User mehr) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nutzer gaben die Nummer mal mit, mal ohne die nationale 0 nach +49 ein (+490151… vs +49151…) -> zwei verschiedene E-Mail-Aliase -> doppelte Accounts. - src/lib/phone.ts: normalizePhoneDE() bringt alles auf E.164 (+49…) und entfernt die Trunk-0; isValidPhoneDE() als leichte Pruefung. - login-form: normalisiert vor dem Senden, validiert, + Hinweistext zum Format. - send-otp / verify-otp: normalisieren serverseitig (die eigentliche Garantie), bevor User/E-Mail-Alias/otp_requests gebildet werden. https://claude.ai/code/session_01G1v5ZGvnrS5hb6eX2hfxKb --- src/components/auth/login-form.tsx | 17 ++++++++++++++--- src/lib/phone.ts | 24 ++++++++++++++++++++++++ supabase/functions/send-otp/index.ts | 15 ++++++++++++++- supabase/functions/verify-otp/index.ts | 14 +++++++++++++- 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 src/lib/phone.ts diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx index 10b97b5..70fb867 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { enableDemoMode } from "@/lib/demo"; +import { normalizePhoneDE, isValidPhoneDE } from "@/lib/phone"; interface LoginFormProps { onSendOtp: (phone: string) => Promise<{ request_id: string }>; @@ -26,7 +27,12 @@ export function LoginForm({ onSendOtp, onVerifyOtp }: LoginFormProps) { setLoading(true); try { - const cleanPhone = phone.replace(/\s/g, ""); + const cleanPhone = normalizePhoneDE(phone); + if (!isValidPhoneDE(cleanPhone)) { + setError("Bitte gib eine gueltige Handynummer ein, z.B. +49 151 12345678"); + setLoading(false); + return; + } const result = await onSendOtp(cleanPhone); setRequestId(result.request_id); setStep("otp"); @@ -49,7 +55,7 @@ export function LoginForm({ onSendOtp, onVerifyOtp }: LoginFormProps) { setLoading(true); try { - const cleanPhone = phone.replace(/\s/g, ""); + const cleanPhone = normalizePhoneDE(phone); await onVerifyOtp(cleanPhone, code, requestId); } catch (err) { setError((err as Error).message || "Hm, der Code passt nicht. Nochmal?"); @@ -80,10 +86,15 @@ export function LoginForm({ onSendOtp, onVerifyOtp }: LoginFormProps) { type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} - placeholder="+49 170 1234567" + placeholder="+49 151 12345678" autoComplete="tel" required /> +
+ Format: +49 151 12345678 — + also mit Laendervorwahl, aber ohne die 0{" "} + nach der +49 (nicht +49 0151…). +

Wir schicken dir den Code via Telegram.

diff --git a/src/lib/phone.ts b/src/lib/phone.ts new file mode 100644 index 0000000..ebfda55 --- /dev/null +++ b/src/lib/phone.ts @@ -0,0 +1,24 @@ +/** + * Normalisiert eine (deutsche) Telefonnummer auf E.164-Format (+49...). + * + * Wichtig: entfernt die nationale Trunk-"0" direkt nach der Laendervorwahl, + * damit "+49 0151 234" und "+49 151 234" dieselbe Nummer ergeben und keine + * doppelten Accounts entstehen. + */ +export function normalizePhoneDE(raw: string): string { + let s = raw.replace(/[^\d+]/g, ""); // nur Ziffern und + + if (!s) return ""; + + if (s.startsWith("00")) s = "+" + s.slice(2); // 0049... -> +49... + if (!s.startsWith("+") && s.startsWith("49")) s = "+" + s; // 49... -> +49... + if (s.startsWith("0")) s = "+49" + s.slice(1); // 0151... -> +49151... + if (!s.startsWith("+")) s = "+49" + s; // 151... -> +49151... + + s = s.replace(/^\+490+/, "+49"); // +490151... -> +49151... + return s; +} + +/** Grobe Plausibilitaetspruefung: +49 gefolgt von 6-13 Ziffern. */ +export function isValidPhoneDE(normalized: string): boolean { + return /^\+49\d{6,13}$/.test(normalized); +} diff --git a/supabase/functions/send-otp/index.ts b/supabase/functions/send-otp/index.ts index cd0ab03..08738e8 100644 --- a/supabase/functions/send-otp/index.ts +++ b/supabase/functions/send-otp/index.ts @@ -7,13 +7,26 @@ const corsHeaders = { "authorization, x-client-info, apikey, content-type", }; +// Normalisiert auf E.164 (+49...) und entfernt die nationale Trunk-0, +// damit "+490151..." und "+49151..." nicht als zwei Accounts enden. +function normalizePhoneDE(raw: string): string { + let s = (raw ?? "").replace(/[^\d+]/g, ""); + if (!s) return ""; + if (s.startsWith("00")) s = "+" + s.slice(2); + if (!s.startsWith("+") && s.startsWith("49")) s = "+" + s; + if (s.startsWith("0")) s = "+49" + s.slice(1); + if (!s.startsWith("+")) s = "+49" + s; + return s.replace(/^\+490+/, "+49"); +} + Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response("ok", { headers: corsHeaders }); } try { - const { phone } = await req.json(); + const body = await req.json(); + const phone = normalizePhoneDE(body.phone); if (!phone || !/^\+\d{8,15}$/.test(phone)) { return new Response( diff --git a/supabase/functions/verify-otp/index.ts b/supabase/functions/verify-otp/index.ts index e5bedb2..759b8cc 100644 --- a/supabase/functions/verify-otp/index.ts +++ b/supabase/functions/verify-otp/index.ts @@ -22,6 +22,18 @@ function emailForPhone(phone: string): string { return `${phone.replace(/\D/g, "")}@${EMAIL_DOMAIN}`; } +// Normalisiert auf E.164 (+49...) und entfernt die nationale Trunk-0, damit +// dieselbe Nummer immer denselben User/E-Mail-Alias ergibt (keine Duplikate). +function normalizePhoneDE(raw: string): string { + let s = (raw ?? "").replace(/[^\d+]/g, ""); + if (!s) return ""; + if (s.startsWith("00")) s = "+" + s.slice(2); + if (!s.startsWith("+") && s.startsWith("49")) s = "+" + s; + if (s.startsWith("0")) s = "+49" + s.slice(1); + if (!s.startsWith("+")) s = "+49" + s; + return s.replace(/^\+490+/, "+49"); +} + // deno-lint-ignore no-explicit-any async function findUserByEmail(supabase: any, email: string) { let page = 1; @@ -47,7 +59,7 @@ Deno.serve(async (req) => { try { const body = await req.json(); - const phone: string | undefined = body.phone; + const phone = normalizePhoneDE(body.phone ?? ""); const code: string | undefined = body.code; let requestId: string | undefined = body.request_id; From 36a2a0035926b63bddec88245f2184ecdde12e45 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 11:10:13 +0000 Subject: [PATCH 2/2] ci: Edge Functions automatisch via GitHub Action deployen Deployt send-otp/verify-otp/send-push bei jedem Push auf main, der supabase/functions/** aendert (oder manuell via workflow_dispatch). Nutzt supabase/setup-cli + SUPABASE_ACCESS_TOKEN (Repo-Secret). Loest das wiederkehrende manuelle Deployen ab. https://claude.ai/code/session_01G1v5ZGvnrS5hb6eX2hfxKb --- .github/workflows/deploy-functions.yml | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/deploy-functions.yml diff --git a/.github/workflows/deploy-functions.yml b/.github/workflows/deploy-functions.yml new file mode 100644 index 0000000..3457feb --- /dev/null +++ b/.github/workflows/deploy-functions.yml @@ -0,0 +1,32 @@ +name: Deploy Edge Functions + +# Deployt die Supabase Edge Functions automatisch, sobald sich etwas unter +# supabase/functions/** (oder deren Config) auf main aendert. Zusaetzlich +# manuell ausloesbar (Actions -> Deploy Edge Functions -> Run workflow). +on: + push: + branches: [main] + paths: + - "supabase/functions/**" + - "supabase/config.toml" + - ".github/workflows/deploy-functions.yml" + workflow_dispatch: {} + +jobs: + deploy: + runs-on: ubuntu-latest + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + PROJECT_REF: ljmaeuarmfazvwoohlxk + steps: + - uses: actions/checkout@v4 + + - uses: supabase/setup-cli@v1 + with: + version: latest + + - name: Deploy Edge Functions + run: | + supabase functions deploy send-otp --project-ref "$PROJECT_REF" + supabase functions deploy verify-otp --project-ref "$PROJECT_REF" + supabase functions deploy send-push --project-ref "$PROJECT_REF"