-
Notifications
You must be signed in to change notification settings - Fork 0
feat(notes): domain access share-modal tab (issue #663) #792
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| /** | ||
| * React Query hooks for the note domain-access flow (epic #657 / issue #663). | ||
| * ノートのドメイン招待 (note_domain_access) フローの React Query フック。 | ||
| */ | ||
| import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; | ||
| import { createApiClient } from "@/lib/api"; | ||
| import type { CreateDomainAccessBody, DomainAccessRow } from "@/lib/api/types"; | ||
|
|
||
| /** | ||
| * Query key factory for domain-access queries. | ||
| * ドメイン招待系クエリのキー工場。 | ||
| */ | ||
| export const domainAccessKeys = { | ||
| all: ["domain-access"] as const, | ||
| listByNote: (noteId: string) => [...domainAccessKeys.all, "note", noteId] as const, | ||
| }; | ||
|
|
||
| /** | ||
| * List domain-access rules for a note (owner / editor). | ||
| * ノートのドメインルール一覧を取得する(owner / editor)。 | ||
| */ | ||
| export function useDomainAccessForNote(noteId: string, enabled = true) { | ||
| const api = createApiClient(); | ||
| return useQuery<DomainAccessRow[]>({ | ||
| queryKey: domainAccessKeys.listByNote(noteId), | ||
| queryFn: () => api.listDomainAccess(noteId), | ||
| enabled: enabled && !!noteId, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Create a new domain-access rule (owner only). Free-email providers are | ||
| * rejected by the server with HTTP 400. | ||
| * ドメインルールを追加する(オーナーのみ)。フリーメール (gmail.com 等) は | ||
| * サーバーが 400 で拒否する。 | ||
| */ | ||
| export function useCreateDomainAccess(noteId: string) { | ||
| const api = createApiClient(); | ||
| const qc = useQueryClient(); | ||
| return useMutation<DomainAccessRow, Error, CreateDomainAccessBody>({ | ||
| mutationFn: (body) => api.createDomainAccess(noteId, body), | ||
| onSuccess: () => { | ||
| qc.invalidateQueries({ queryKey: domainAccessKeys.listByNote(noteId) }); | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Delete an existing domain-access rule (owner only). The effect is immediate; | ||
| * any user who was relying on this rule loses access on their next request. | ||
| * ドメインルールを削除する(オーナーのみ)。削除は即座に反映され、その | ||
| * ドメインに依存していたアクセスは次回リクエストから失効する。 | ||
| */ | ||
| export function useDeleteDomainAccess(noteId: string) { | ||
| const api = createApiClient(); | ||
| const qc = useQueryClient(); | ||
| return useMutation<{ removed: true; id: string }, Error, { accessId: string }>({ | ||
| mutationFn: ({ accessId }) => api.deleteDomainAccess(noteId, accessId), | ||
| onSuccess: () => { | ||
| qc.invalidateQueries({ queryKey: domainAccessKeys.listByNote(noteId) }); | ||
| }, | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /** | ||
| * Tests for the client-side domain validator (issue #663). | ||
| * クライアント側ドメイン検証のテスト。 | ||
| */ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { normalizeDomainInput } from "./domainValidation"; | ||
|
|
||
| describe("normalizeDomainInput", () => { | ||
| it("trims, lower-cases, and accepts a plain domain", () => { | ||
| const result = normalizeDomainInput(" Example.COM "); | ||
| expect(result).toEqual({ ok: true, domain: "example.com" }); | ||
| }); | ||
|
|
||
| it("strips a single leading @ from email-style input", () => { | ||
| const result = normalizeDomainInput("@example.com"); | ||
| expect(result).toEqual({ ok: true, domain: "example.com" }); | ||
| }); | ||
|
|
||
| it("flags empty strings as empty", () => { | ||
| expect(normalizeDomainInput("")).toEqual({ ok: false, error: { kind: "empty" } }); | ||
| expect(normalizeDomainInput(" ")).toEqual({ ok: false, error: { kind: "empty" } }); | ||
| expect(normalizeDomainInput(undefined)).toEqual({ ok: false, error: { kind: "empty" } }); | ||
| }); | ||
|
|
||
| it("rejects malformed domains", () => { | ||
| expect(normalizeDomainInput("not-a-domain")).toEqual({ | ||
| ok: false, | ||
| error: { kind: "invalid_format" }, | ||
| }); | ||
| expect(normalizeDomainInput("example.")).toEqual({ | ||
| ok: false, | ||
| error: { kind: "invalid_format" }, | ||
| }); | ||
| }); | ||
|
|
||
| it("rejects free webmail providers (gmail, outlook, yahoo, …)", () => { | ||
| expect(normalizeDomainInput("gmail.com")).toEqual({ | ||
| ok: false, | ||
| error: { kind: "free_email", domain: "gmail.com" }, | ||
| }); | ||
| expect(normalizeDomainInput("@yahoo.co.jp")).toEqual({ | ||
| ok: false, | ||
| error: { kind: "free_email", domain: "yahoo.co.jp" }, | ||
| }); | ||
| expect(normalizeDomainInput("OUTLOOK.com")).toEqual({ | ||
| ok: false, | ||
| error: { kind: "free_email", domain: "outlook.com" }, | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| /** | ||
| * クライアント側のドメイン入力検証 (`note_domain_access`, issue #663)。 | ||
| * サーバー側 `server/api/src/lib/freeEmailDomains.ts` のロジックをミラーし、 | ||
| * UI でのインライン警告に使う。最終判定はサーバー側で行うため、これは | ||
| * 「送信前に明確に弾けるものを早めに弾く」目的のソフトな検証。 | ||
| * | ||
| * Client-side domain input validation for `note_domain_access` (issue #663). | ||
| * Mirrors `server/api/src/lib/freeEmailDomains.ts` so the share-modal domain | ||
| * tab can warn users before they submit. The server remains the source of | ||
| * truth — this is intentionally a soft pre-check. | ||
| */ | ||
|
|
||
| /** | ||
| * 拒否対象の無料メールドメイン(小文字・`@` なし)。サーバーの拒否リストと | ||
| * 同期させる。差分が出てもサーバーが最終判定するため致命的ではない。 | ||
| * | ||
| * Free-webmail providers blocked for domain rules. Kept in sync with the | ||
| * server list; small drift is non-fatal because the server enforces the truth. | ||
| */ | ||
| export const FREE_EMAIL_DOMAINS: ReadonlySet<string> = new Set([ | ||
| "gmail.com", | ||
| "googlemail.com", | ||
| // Microsoft | ||
| "outlook.com", | ||
| "outlook.jp", | ||
| "hotmail.com", | ||
| "hotmail.co.jp", | ||
| "live.com", | ||
| "live.jp", | ||
| "msn.com", | ||
| // Yahoo | ||
| "yahoo.com", | ||
| "yahoo.co.jp", | ||
| "ymail.com", | ||
| // Apple | ||
| "icloud.com", | ||
| "me.com", | ||
| "mac.com", | ||
| // Other major free webmail | ||
| "aol.com", | ||
| "proton.me", | ||
| "protonmail.com", | ||
| "pm.me", | ||
| "gmx.com", | ||
| "gmx.net", | ||
| "mail.com", | ||
| "zoho.com", | ||
| "yandex.com", | ||
| "yandex.ru", | ||
| // Japanese carriers / ISP free tiers | ||
| "docomo.ne.jp", | ||
| "ezweb.ne.jp", | ||
| "softbank.ne.jp", | ||
| "i.softbank.jp", | ||
| "ybb.ne.jp", | ||
| "nifty.com", | ||
| "so-net.ne.jp", | ||
| "biglobe.ne.jp", | ||
| "ocn.ne.jp", | ||
| // Disposable / throwaway (representative) | ||
| "mailinator.com", | ||
| "guerrillamail.com", | ||
| "10minutemail.com", | ||
| "tempmail.com", | ||
| "trashmail.com", | ||
| ]); | ||
|
|
||
| /** | ||
| * ドメイン検証エラーの判別共用体。 | ||
| * Discriminated error kinds for client-side domain validation. | ||
| */ | ||
| export type DomainValidationError = | ||
| | { kind: "empty" } | ||
| | { kind: "invalid_format" } | ||
| | { kind: "free_email"; domain: string }; | ||
|
|
||
| /** | ||
| * 入力検証の結果。成功時は正規化済みドメイン、失敗時は理由。 | ||
| * Validation result — normalised domain on success, otherwise an error kind. | ||
| */ | ||
| export type DomainValidationResult = | ||
| | { ok: true; domain: string } | ||
| | { ok: false; error: DomainValidationError }; | ||
|
|
||
| /** | ||
| * RFC 1035 ベースのラフなドメイン検証。サーバーのものと同一。 | ||
| * Lightweight RFC 1035 domain check; mirrors the server regex. | ||
| */ | ||
| const DOMAIN_REGEX = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/; | ||
|
|
||
| /** | ||
| * 生のドメイン入力を正規化・検証する。 | ||
| * | ||
| * Normalise and validate a raw domain input: | ||
| * - trim & lower-case | ||
| * - strip a single leading `@` (so `@example.com` and `example.com` both work) | ||
| * - reject empty strings, malformed domains, and free-webmail providers. | ||
| * | ||
| * @param raw - ユーザー入力 / Raw user input. | ||
| */ | ||
| export function normalizeDomainInput(raw: unknown): DomainValidationResult { | ||
| if (typeof raw !== "string") { | ||
| return { ok: false, error: { kind: "empty" } }; | ||
| } | ||
| let value = raw.trim().toLowerCase(); | ||
| if (value.startsWith("@")) { | ||
| value = value.slice(1); | ||
| } | ||
| if (value.length === 0) { | ||
| return { ok: false, error: { kind: "empty" } }; | ||
| } | ||
| if (!DOMAIN_REGEX.test(value)) { | ||
| return { ok: false, error: { kind: "invalid_format" } }; | ||
| } | ||
| if (FREE_EMAIL_DOMAINS.has(value)) { | ||
| return { ok: false, error: { kind: "free_email", domain: value } }; | ||
| } | ||
| return { ok: true, domain: value }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.