diff --git a/CLAUDE.md b/CLAUDE.md index dd04de11..376ad3a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,6 +151,21 @@ restores the image but never the schema, so keep migrations additive. (`/srv/www/shared/owner-reports` -- not `shared/reports`, which is the analytics'), and `internal/boards/survival_test.go` runs every importer over stored reports. +- `internal/club` -- **the OpenIPC Club**: signing in with Telegram (the bot + takes Start with a code the asking browser shows, then tells the member + what happened to what they sent), GitHub (a member of `CLUB_MAINTAINER_ORG` + reviews) or an emailed link, each only when configured + (`TELEGRAM_BOT_TOKEN`, `GITHUB_OAUTH_*`, `CLUB_SMTP_*`). Mail goes to the + host's own exim at `172.18.0.1:25`, which says HELO as `webber-eu.openipc.org` + (its PTR) and signs DKIM selector `webber2026`; openipc.org's SPF names + 37.27.251.71 and DMARC is `p=none` (Hetzner DNS). Accounts and sessions + only: a member's reports, their private dumps, the review queue at + `/club/review` and the stars ledger (`report_stars`, written by a review's + decision, never by an upload) are `internal/reports`'. The board panel's + send form posts to `/api/v1/club/reports` instead of opening a GitHub + issue, and a published report's text and photos become a contributed unit + on its board (`boards.ApplyReportUnits`), as `boards/contributions.yml` + does for what arrived as issues (#365, #366). - `internal/tools`, `internal/nfsro` — **ipctool for stock firmware**, which has no curl and no TLS. ipctool's release job pushes each build to `PUT /api/v1/tools/{name}` (OIDC, `internal/tools/PUSH.md`); nginx serves @@ -185,8 +200,10 @@ restores the image but never the schema, so keep migrations additive. ### Conventions & gotchas -- There is no admin and no sign-in (#288). `/admin` answers 410; nothing on the - site sets a cookie. +- There is no admin (#288): `/admin` answers 410. The one sign-in is the + OpenIPC Club (`internal/club`), and its session cookie's path is + `/api/v1/club`: pages stay static and cached, and a visitor who never signs + in is never sent a cookie. - `deploy/static/reserved-paths` lists the addresses a bundle file must never shadow (the upload, the firmware download, the service's APIs, nginx's own locations); `service/deploytest` derives what must be in it and fails when an diff --git a/data/locales/boards.en.yml b/data/locales/boards.en.yml index 0c6217d9..cca71218 100644 --- a/data/locales/boards.en.yml +++ b/data/locales/boards.en.yml @@ -30,6 +30,14 @@ en: fact2: "The MAC, the chip's serial and the cloud ID are replaced with hashes in everything published, in the report and in any log." fact3_title: "A backup is private unless you say otherwise" fact3_html: "It holds everything the camera stores, including Wi-Fi keys and passwords. It is published only if you sent it with --public." + club: + eyebrow: 'OpenIPC Club' + title: 'OpenIPC Club' + lede: 'Sign in to follow what you sent to the board catalogue, keep your flash dumps private, and collect stars for what is accepted.' + club_review: + eyebrow: 'OpenIPC Club' + title: 'Review queue' + lede: 'Owner reports waiting for a maintainer''s decision.' nav: boards: Camera boards cameras: @@ -373,4 +381,125 @@ en: new_tab: opens in a new tab files_title: Photos and files from_source: 'From {source}' + sent_by: 'Sent by {who}' known_sources: 'Sources: ' + club: + eyebrow: 'OpenIPC Club' + loading: 'Loading…' + load_failed: 'The club could not be reached. Try again in a minute.' + why_title: 'Why sign in' + why_1: 'See whether what you sent was accepted, and where it appears.' + why_2: 'Keep your flash dumps private: only you and OpenIPC''s maintainers can download them.' + why_3: 'Collect stars, and your name next to the boards you documented.' + why_note: 'You don''t need an account to browse or download firmware. The site sets no cookie for anyone who doesn''t sign in.' + tg_button: 'Continue with Telegram' + tg_hint: 'No password: tap Start in the OpenIPC bot' + gh_button: 'Continue with GitHub' + gh_hint: 'Maintainers sign in here' + or: 'or' + email_label: 'We''ll email you a link that signs you in. No password.' + email_placeholder: 'you@example.com' + email_button: 'Email me a link' + email_sent: 'Check {email}: the link signs you in once, within ten minutes.' + unavailable: 'Not available on this site yet.' + tg_title: 'Sign in with Telegram' + tg_step1: 'Scan the code with your phone''s camera, or open Telegram on this computer.' + tg_step2: 'Tap Start in the chat with {bot}, then Yes.' + tg_step3: 'Come back here. This page signs you in by itself.' + tg_open: 'Open Telegram' + tg_other: 'Use GitHub or email instead' + tg_waiting: 'Waiting for Telegram… This code works for {time} more.' + tg_expired: 'The code expired.' + tg_retry: 'Get a new code' + close: 'Close' + signin_expired: 'That sign-in link has expired or was already used. Start again below.' + signin_elsewhere: 'That sign-in belongs to the browser that started it. Start again here.' + signin_failed: 'The sign-in did not complete. Try again, or use another way in.' + pending: '+{n} waiting for review' + ways: 'Signed in with' + link_more: 'Add another way in' + provider_telegram: 'Telegram' + provider_github: 'GitHub' + provider_email: 'Email' + bot_on: 'The OpenIPC bot tells you when something you sent is reviewed.' + bot_muted: 'The bot''s messages are muted.' + mute: 'Mute the bot' + unmute: 'Unmute the bot' + sign_out: 'Sign out' + review_link: 'Review queue' + mine_title: 'My submissions' + mine_empty: 'Nothing sent yet. Open your board in the catalogue and send what it prints.' + boards_link: 'Camera boards' + col_submission: 'Submission' + col_status: 'Status' + col_stars: 'Stars' + status_pending: 'Waiting for review' + status_published: 'Accepted' + status_rejected: 'Not accepted' + status_withdrawn: 'Withdrawn' + duplicate: 'The catalogue already has this dump' + private: 'Only you and maintainers' + kind_backup: 'Full flash dump' + kind_photo: 'Photo' + kind_boot_log: 'Boot log' + kind_uboot_env: 'U-Boot console' + kind_note: 'Note' + kind_document: 'Document' + kind_yaml: 'ipctool report' + no_board: 'No board named' + rules_title: 'How stars work' + rules_item: 'Boot log, U-Boot console, photo, pinout, ipctool report' + rules_dump: 'Full flash dump the catalogue doesn''t have' + rules_none: 'A dump the catalogue already holds, or anything not accepted' + rules_note: 'Stars count when a maintainer accepts what you sent, never on upload.' + send_kinds_label: 'What you are sending' + send_kind_boot_log: 'Boot log' + send_kind_uboot_env: 'U-Boot console' + send_kind_photo: 'Photos' + send_kind_backup: 'Full flash dump' + send_paste: 'Paste the text here, or choose a file below' + send_file: 'File' + send_photos: 'Photos: the front, the back, the UART pins' + send_dump: 'The whole chip, as a programmer read it or as an ipctool backup' + send_public: 'Publish the dump with the report. It holds Wi-Fi keys and passwords: leave this off to keep it private.' + send_note: 'Anything else: where you bought it, what it is sold as' + send_button: 'Send to the catalogue' + sending: 'Sending…' + sent_member: 'Received as {id}. A maintainer reviews it before it is shown; follow it in My submissions.' + sent_guest: 'Received as {id}. A maintainer reviews it before it is shown.' + receipt: 'Receipt' + my_link: 'My submissions' + sign_in_link: 'Sign in' + send_signed_in: 'Signed in as {name}. Stars count once a maintainer accepts it.' + send_guest: 'You can send without signing in. Sign in to collect stars and follow what happens to it.' + send_failed: 'Not sent: {error}' + send_empty: 'Add a file, a photo or some text first.' + reward: '+{n} ★' + send_another: 'Send something else' + review_title: 'Review queue' + review_empty: 'Nothing is waiting.' + review_forbidden: 'Only OpenIPC''s maintainers review. Sign in with GitHub as a member of the OpenIPC organisation.' + review_from: 'From {who}' + review_anon: 'Sent without signing in' + review_board: 'Board named by the sender' + review_guess: 'ipctool says it looks like {board}' + review_models: 'Board ids to publish it on, comma-separated (empty: the board the sender named)' + review_note: 'Note for the sender' + publish: 'Publish' + reject: 'Reject' + review_points: '{points} ★' + review_done_publish: 'Published. {points} ★ to the sender.' + review_done_reject: 'Rejected.' + show_yaml: 'ipctool output' + tab_pending: 'Waiting' + tab_published: 'Published' + tab_rejected: 'Rejected' + header_club: 'Club' + confirm_title: 'Sign in as {who}?' + confirm_text: 'This link was asked for in another browser. Sign in only if {who} is yours: whatever you send while signed in goes to that account.' + confirm_button: 'Sign in' + confirm_cancel: 'Cancel' + rename: 'Change name' + rename_label: 'Your name, as it appears on the boards your reports reach' + rename_save: 'Save' + review_note_label: 'Reviewer' diff --git a/data/locales/boards.ru.yml b/data/locales/boards.ru.yml index a4e52bd3..153c3c44 100644 --- a/data/locales/boards.ru.yml +++ b/data/locales/boards.ru.yml @@ -30,6 +30,14 @@ ru: fact2: "MAC, серийный номер чипа и облачный ID во всём опубликованном заменены хешами — и в отчёте, и в логах." fact3_title: "Бэкап закрыт, если вы не решите иначе" fact3_html: "В нём всё, что хранит камера, включая ключи Wi-Fi и пароли. Он публикуется, только если вы отправили его с --public." + club: + eyebrow: 'Клуб OpenIPC' + title: 'Клуб OpenIPC' + lede: 'Войдите, чтобы следить за тем, что вы прислали в каталог плат, держать дампы флеш-памяти закрытыми и получать звёзды за принятое.' + club_review: + eyebrow: 'Клуб OpenIPC' + title: 'Очередь проверки' + lede: 'Отчёты владельцев, которые ждут решения мейнтейнера.' nav: boards: Платы камер cameras: @@ -414,4 +422,125 @@ ru: new_tab: откроется в новой вкладке files_title: Фото и файлы from_source: 'Источник: {source}' + sent_by: 'Прислано: {who}' known_sources: 'Источники: ' + club: + eyebrow: 'Клуб OpenIPC' + loading: 'Загрузка…' + load_failed: 'Не удалось связаться с клубом. Попробуйте через минуту.' + why_title: 'Зачем входить' + why_1: 'Видеть, приняли ли присланное и где оно появилось.' + why_2: 'Держать дампы флеш-памяти закрытыми: скачать их можете только вы и мейнтейнеры OpenIPC.' + why_3: 'Получать звёзды и видеть своё имя рядом с платами, которые вы описали.' + why_note: 'Чтобы смотреть каталог и скачивать прошивки, учётная запись не нужна. Тем, кто не входит, сайт не ставит ни одной cookie.' + tg_button: 'Войти через Telegram' + tg_hint: 'Без пароля: нажмите «Старт» в боте OpenIPC' + gh_button: 'Войти через GitHub' + gh_hint: 'Мейнтейнеры входят здесь' + or: 'или' + email_label: 'Пришлём на почту ссылку для входа. Без пароля.' + email_placeholder: 'you@example.com' + email_button: 'Прислать ссылку' + email_sent: 'Проверьте {email}: ссылка выполнит вход один раз в течение десяти минут.' + unavailable: 'На этом сайте пока недоступно.' + tg_title: 'Вход через Telegram' + tg_step1: 'Отсканируйте код камерой телефона или откройте Telegram на этом компьютере.' + tg_step2: 'Нажмите «Старт» в чате с {bot}, затем «Да».' + tg_step3: 'Вернитесь сюда: страница выполнит вход сама.' + tg_open: 'Открыть Telegram' + tg_other: 'Войти через GitHub или почту' + tg_waiting: 'Ждём Telegram… Код действует ещё {time}.' + tg_expired: 'Код устарел.' + tg_retry: 'Получить новый код' + close: 'Закрыть' + signin_expired: 'Ссылка для входа устарела или уже использована. Начните заново ниже.' + signin_elsewhere: 'Этот вход принадлежит браузеру, в котором он начат. Начните заново здесь.' + signin_failed: 'Вход не завершился. Попробуйте ещё раз или выберите другой способ.' + pending: '+{n} ждут проверки' + ways: 'Способы входа' + link_more: 'Добавить способ входа' + provider_telegram: 'Telegram' + provider_github: 'GitHub' + provider_email: 'Почта' + bot_on: 'Бот OpenIPC сообщает, когда присланное проверено.' + bot_muted: 'Сообщения бота отключены.' + mute: 'Отключить бота' + unmute: 'Включить бота' + sign_out: 'Выйти' + review_link: 'Очередь проверки' + mine_title: 'Мои материалы' + mine_empty: 'Вы пока ничего не присылали. Откройте свою плату в каталоге и пришлите то, что она выводит.' + boards_link: 'Платы камер' + col_submission: 'Материал' + col_status: 'Статус' + col_stars: 'Звёзды' + status_pending: 'Ждёт проверки' + status_published: 'Принято' + status_rejected: 'Не принято' + status_withdrawn: 'Отозвано' + duplicate: 'Такой дамп в каталоге уже есть' + private: 'Только вам и мейнтейнерам' + kind_backup: 'Полный дамп флеш-памяти' + kind_photo: 'Фото' + kind_boot_log: 'Лог загрузки' + kind_uboot_env: 'Консоль U-Boot' + kind_note: 'Заметка' + kind_document: 'Документ' + kind_yaml: 'Отчёт ipctool' + no_board: 'Плата не указана' + rules_title: 'Как начисляются звёзды' + rules_item: 'Лог загрузки, консоль U-Boot, фото, распиновка, отчёт ipctool' + rules_dump: 'Полный дамп флеш-памяти, которого нет в каталоге' + rules_none: 'Дамп, который уже есть в каталоге, или непринятый материал' + rules_note: 'Звёзды начисляются, когда мейнтейнер принимает присланное, а не при загрузке.' + send_kinds_label: 'Что вы присылаете' + send_kind_boot_log: 'Лог загрузки' + send_kind_uboot_env: 'Консоль U-Boot' + send_kind_photo: 'Фото' + send_kind_backup: 'Полный дамп флеш-памяти' + send_paste: 'Вставьте текст сюда или выберите файл ниже' + send_file: 'Файл' + send_photos: 'Фото: лицевая сторона, обратная, контакты UART' + send_dump: 'Вся микросхема: как её считал программатор, или резервная копия ipctool' + send_public: 'Опубликовать дамп вместе с отчётом. В нём ключи Wi-Fi и пароли: не отмечайте, чтобы он остался закрытым.' + send_note: 'Что-нибудь ещё: где купили, под каким названием продаётся' + send_button: 'Отправить в каталог' + sending: 'Отправляем…' + sent_member: 'Получено, номер {id}. Мейнтейнер проверит перед публикацией; следите в разделе «Мои материалы».' + sent_guest: 'Получено, номер {id}. Мейнтейнер проверит перед публикацией.' + receipt: 'Квитанция' + my_link: 'Мои материалы' + sign_in_link: 'Войти' + send_signed_in: 'Вы вошли как {name}. Звёзды начислятся, когда мейнтейнер примет присланное.' + send_guest: 'Отправить можно и без входа. Войдите, чтобы получать звёзды и следить за проверкой.' + send_failed: 'Не отправлено: {error}' + send_empty: 'Сначала добавьте файл, фото или текст.' + reward: '+{n} ★' + send_another: 'Прислать что-то ещё' + review_title: 'Очередь проверки' + review_empty: 'Ничего не ждёт проверки.' + review_forbidden: 'Проверяют только мейнтейнеры OpenIPC. Войдите через GitHub как участник организации OpenIPC.' + review_from: 'От {who}' + review_anon: 'Прислано без входа' + review_board: 'Плата, указанная отправителем' + review_guess: 'По данным ipctool похоже на {board}' + review_models: 'Id плат для публикации через запятую (пусто — плата, указанная отправителем)' + review_note: 'Пояснение для отправителя' + publish: 'Опубликовать' + reject: 'Отклонить' + review_points: '{points} ★' + review_done_publish: 'Опубликовано. Отправителю {points} ★.' + review_done_reject: 'Отклонено.' + show_yaml: 'Вывод ipctool' + tab_pending: 'Ждут' + tab_published: 'Опубликованы' + tab_rejected: 'Отклонены' + header_club: 'Клуб' + confirm_title: 'Войти как {who}?' + confirm_text: 'Эту ссылку запросили в другом браузере. Входите, только если {who} — это вы: всё, что вы пришлёте после входа, попадёт в эту учётную запись.' + confirm_button: 'Войти' + confirm_cancel: 'Отмена' + rename: 'Изменить имя' + rename_label: 'Ваше имя — так оно показывается у плат, куда попали ваши материалы' + rename_save: 'Сохранить' + review_note_label: 'Проверяющий' diff --git a/data/locales/boards.zh.yml b/data/locales/boards.zh.yml index d470826b..7d265445 100644 --- a/data/locales/boards.zh.yml +++ b/data/locales/boards.zh.yml @@ -30,6 +30,14 @@ zh: fact2: "在所有公开内容中(报告和日志),MAC、芯片序列号和云 ID 都会被替换为哈希值。" fact3_title: "备份默认不公开" fact3_html: "备份包含摄像头存储的一切,包括 Wi-Fi 密钥和密码。只有使用 --public 发送时才会公开。" + club: + eyebrow: 'OpenIPC 俱乐部' + title: 'OpenIPC 俱乐部' + lede: '登录后可跟踪你提交到电路板目录的内容、让固件转储保持私密,并为通过审核的内容获得星星。' + club_review: + eyebrow: 'OpenIPC 俱乐部' + title: '审核队列' + lede: '等待维护者审核的用户报告。' nav: boards: 摄像头主板 cameras: @@ -376,4 +384,125 @@ zh: new_tab: 在新标签页中打开 files_title: 照片与文件 from_source: '来源:{source}' + sent_by: '{who} 提供' known_sources: '来源:' + club: + eyebrow: 'OpenIPC 俱乐部' + loading: '加载中…' + load_failed: '无法连接俱乐部服务,请稍后再试。' + why_title: '为什么要登录' + why_1: '查看你提交的内容是否通过审核,以及出现在哪里。' + why_2: '让固件转储保持私密:只有你和 OpenIPC 维护者可以下载。' + why_3: '获得星星,并在你记录的电路板旁显示你的名字。' + why_note: '浏览目录或下载固件无需账户。对不登录的访客,本站不设置任何 cookie。' + tg_button: '使用 Telegram 登录' + tg_hint: '无需密码:在 OpenIPC 机器人中点击“开始”' + gh_button: '使用 GitHub 登录' + gh_hint: '维护者从这里登录' + or: '或' + email_label: '我们会通过邮件发送登录链接,无需密码。' + email_placeholder: 'you@example.com' + email_button: '发送登录链接' + email_sent: '请查收 {email}:链接仅可使用一次,十分钟内有效。' + unavailable: '本站暂不可用。' + tg_title: '使用 Telegram 登录' + tg_step1: '用手机相机扫描二维码,或在本电脑上打开 Telegram。' + tg_step2: '在与 {bot} 的对话中点击“开始”,然后点“是”。' + tg_step3: '回到此页面,它会自动完成登录。' + tg_open: '打开 Telegram' + tg_other: '改用 GitHub 或邮箱' + tg_waiting: '正在等待 Telegram… 此码还剩 {time} 有效。' + tg_expired: '此码已过期。' + tg_retry: '获取新码' + close: '关闭' + signin_expired: '该登录链接已过期或已被使用,请在下方重新开始。' + signin_elsewhere: '该登录只属于发起它的浏览器,请在此重新开始。' + signin_failed: '登录未完成,请重试或换一种方式。' + pending: '+{n} 等待审核' + ways: '登录方式' + link_more: '添加其他登录方式' + provider_telegram: 'Telegram' + provider_github: 'GitHub' + provider_email: '邮箱' + bot_on: '提交内容被审核后,OpenIPC 机器人会通知你。' + bot_muted: '机器人消息已静音。' + mute: '静音机器人' + unmute: '取消静音' + sign_out: '退出登录' + review_link: '审核队列' + mine_title: '我的提交' + mine_empty: '还没有提交。在目录中打开你的电路板,提交它的输出。' + boards_link: '摄像头电路板' + col_submission: '提交内容' + col_status: '状态' + col_stars: '星星' + status_pending: '等待审核' + status_published: '已通过' + status_rejected: '未通过' + status_withdrawn: '已撤回' + duplicate: '目录中已有此转储' + private: '仅你和维护者可见' + kind_backup: '完整闪存转储' + kind_photo: '照片' + kind_boot_log: '启动日志' + kind_uboot_env: 'U-Boot 控制台' + kind_note: '备注' + kind_document: '文档' + kind_yaml: 'ipctool 报告' + no_board: '未指定电路板' + rules_title: '星星如何计算' + rules_item: '启动日志、U-Boot 控制台、照片、引脚图、ipctool 报告' + rules_dump: '目录中没有的完整闪存转储' + rules_none: '目录中已有的转储,或未通过的内容' + rules_note: '维护者通过你的提交时才计星,上传时不计。' + send_kinds_label: '提交内容类型' + send_kind_boot_log: '启动日志' + send_kind_uboot_env: 'U-Boot 控制台' + send_kind_photo: '照片' + send_kind_backup: '完整闪存转储' + send_paste: '在此粘贴文本,或在下方选择文件' + send_file: '文件' + send_photos: '照片:正面、背面、UART 引脚' + send_dump: '整颗芯片:编程器读出的镜像,或 ipctool 备份' + send_public: '随报告公开此转储。其中包含 Wi-Fi 密钥和密码:不勾选则保持私密。' + send_note: '其他信息:购买渠道、商品名称' + send_button: '提交到目录' + sending: '提交中…' + sent_member: '已收到,编号 {id}。维护者审核后才会公开;可在“我的提交”中跟踪。' + sent_guest: '已收到,编号 {id}。维护者审核后才会公开。' + receipt: '回执' + my_link: '我的提交' + sign_in_link: '登录' + send_signed_in: '已以 {name} 登录。维护者通过后计星。' + send_guest: '无需登录也可提交。登录后可获得星星并跟踪审核进度。' + send_failed: '未提交:{error}' + send_empty: '请先添加文件、照片或文本。' + reward: '+{n} ★' + send_another: '再提交其他内容' + review_title: '审核队列' + review_empty: '没有待审核的内容。' + review_forbidden: '只有 OpenIPC 维护者可以审核。请以 OpenIPC 组织成员身份使用 GitHub 登录。' + review_from: '来自 {who}' + review_anon: '未登录提交' + review_board: '提交者指定的电路板' + review_guess: 'ipctool 显示类似 {board}' + review_models: '发布到的电路板 id,用逗号分隔(留空:提交者指定的电路板)' + review_note: '给提交者的说明' + publish: '发布' + reject: '拒绝' + review_points: '{points} ★' + review_done_publish: '已发布。提交者获得 {points} ★。' + review_done_reject: '已拒绝。' + show_yaml: 'ipctool 输出' + tab_pending: '待审核' + tab_published: '已发布' + tab_rejected: '已拒绝' + header_club: '俱乐部' + confirm_title: '以 {who} 身份登录?' + confirm_text: '此链接是在另一个浏览器中请求的。只有当 {who} 是你本人时才登录:登录后提交的内容都会进入该账户。' + confirm_button: '登录' + confirm_cancel: '取消' + rename: '修改名字' + rename_label: '你的名字,会显示在你的提交所属的电路板上' + rename_save: '保存' + review_note_label: '审核者' diff --git a/deploy/nginx/sites-available/org.openipc b/deploy/nginx/sites-available/org.openipc index 1d102f5c..aaafde00 100644 --- a/deploy/nginx/sites-available/org.openipc +++ b/deploy/nginx/sites-available/org.openipc @@ -400,6 +400,27 @@ server { add_header X-Served-By go always; } + # The OpenIPC Club (service/internal/club): signing in, the send form, + # members' own reports and the maintainers' review. Its session cookie's + # path is this prefix, so nothing else on the site ever receives it, and + # nothing here is cached: every answer is about one browser. The send + # form can carry a flash dump, so the body is streamed as for reports. + location ^~ /api/v1/club/ { + client_max_body_size 300m; + client_body_timeout 300; + proxy_request_buffering off; + proxy_read_timeout 300; + proxy_send_timeout 300; + limit_req zone=boards_search burst=30 nodelay; + limit_req_status 429; + proxy_pass http://127.0.0.1:3002; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Request-Id $request_id; + add_header Strict-Transport-Security max-age=15768000; + add_header X-Served-By go always; + } + # Which catalogue boards ipctool's output may be from. Stores nothing. location = /api/v1/boards/identify { client_max_body_size 256k; diff --git a/deploy/nginx/sites-available/org.openipc.dev b/deploy/nginx/sites-available/org.openipc.dev index 763de940..83d5e3ca 100644 --- a/deploy/nginx/sites-available/org.openipc.dev +++ b/deploy/nginx/sites-available/org.openipc.dev @@ -350,6 +350,42 @@ server { add_header X-Robots-Tag "noindex, nofollow, noarchive" always; } + # The OpenIPC Club (service/internal/club): signing in, the send form, + # members' own reports and the maintainers' review. Its session cookie's + # path is this prefix, so nothing else on the site ever receives it, and + # nothing here is cached: every answer is about one browser. The send + # form can carry a flash dump, so the body is streamed as for reports. + location ^~ /api/v1/club/ { + client_max_body_size 300m; + client_body_timeout 300; + proxy_request_buffering off; + proxy_read_timeout 300; + proxy_send_timeout 300; + limit_req zone=boards_search burst=30 nodelay; + limit_req_status 429; + proxy_pass http://127.0.0.1:3012; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Request-Id $request_id; + add_header Strict-Transport-Security max-age=15768000; + add_header X-Served-By go always; + add_header X-Robots-Tag "noindex, nofollow, noarchive" always; + } + + # Telegram delivers what people write to the dev bot here, and Telegram + # has no staging password: the web role checks the bot's secret header. + location = /api/v1/club/telegram/webhook { + auth_basic off; + client_max_body_size 64k; + proxy_pass http://127.0.0.1:3012; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Request-Id $request_id; + add_header Strict-Transport-Security max-age=15768000; + add_header X-Served-By go always; + add_header X-Robots-Tag "noindex, nofollow, noarchive" always; + } + # Which catalogue boards ipctool's output may be from. Stores nothing. location = /api/v1/boards/identify { client_max_body_size 256k; diff --git a/frontend/apps/site/src/components/SiteHeader.astro b/frontend/apps/site/src/components/SiteHeader.astro index 8774a169..62be68ad 100644 --- a/frontend/apps/site/src/components/SiteHeader.astro +++ b/frontend/apps/site/src/components/SiteHeader.astro @@ -26,6 +26,7 @@ */ import { LOCALE_NAMES, alternates, pathFor, useTranslations, type Locale } from '../lib/i18n'; import { menuFor } from '../lib/nav'; +import ClubBadge from './club/ClubBadge.tsx'; import logo from '../assets/openipc-logo-white.svg'; import translate from '../assets/icons/translate.svg?raw'; import github from '../assets/icons/github.svg?raw'; @@ -98,6 +99,10 @@ const alt = alternates(path); ))} + +