feat(web): AuthContext + клиент под cookie-сессии, projectId из контекста
This commit is contained in:
+95
-10
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { api } from "./client"
|
||||
import { DEFAULT_PROJECT_ID } from "@/lib/config"
|
||||
import { api, UnauthorizedError } from "./client"
|
||||
|
||||
const PROJECT_ID = "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
beforeEach(() => { vi.restoreAllMocks() })
|
||||
|
||||
@@ -13,19 +14,72 @@ function mockFetch(body: unknown, ok = true, status = 200) {
|
||||
}
|
||||
|
||||
describe("api client", () => {
|
||||
it("lists accounts at project-scoped path", async () => {
|
||||
it("sends credentials:include on every request", async () => {
|
||||
const spy = mockFetch([])
|
||||
await api.listAccounts(PROJECT_ID)
|
||||
const [, opts] = spy.mock.calls[0]
|
||||
expect((opts as RequestInit).credentials).toBe("include")
|
||||
})
|
||||
|
||||
describe("api.auth", () => {
|
||||
it("login POSTs to /api/v1/auth/login with credentials:include", async () => {
|
||||
const spy = mockFetch({ user: { id: "u1", email: "a@b.com" }, project: { id: "p1", name: "Default" } })
|
||||
await api.auth.login("a@b.com", "secret")
|
||||
const [url, opts] = spy.mock.calls[0]
|
||||
expect(url).toBe("/api/v1/auth/login")
|
||||
expect((opts as RequestInit).method).toBe("POST")
|
||||
expect((opts as RequestInit).credentials).toBe("include")
|
||||
expect(String((opts as RequestInit).body)).toContain("secret")
|
||||
})
|
||||
|
||||
it("register POSTs to /api/v1/auth/register", async () => {
|
||||
const spy = mockFetch({ user: { id: "u1", email: "a@b.com" }, project: { id: "p1", name: "Default" } })
|
||||
await api.auth.register("a@b.com", "secret")
|
||||
const [url, opts] = spy.mock.calls[0]
|
||||
expect(url).toBe("/api/v1/auth/register")
|
||||
expect((opts as RequestInit).method).toBe("POST")
|
||||
})
|
||||
|
||||
it("logout POSTs to /api/v1/auth/logout", async () => {
|
||||
const spy = mockFetch(undefined, true, 204)
|
||||
await api.auth.logout()
|
||||
const [url, opts] = spy.mock.calls[0]
|
||||
expect(url).toBe("/api/v1/auth/logout")
|
||||
expect((opts as RequestInit).method).toBe("POST")
|
||||
})
|
||||
|
||||
it("me GETs /api/v1/auth/me and returns AuthState", async () => {
|
||||
const state = { user: { id: "u1", email: "a@b.com" }, project: { id: "p1", name: "Default" } }
|
||||
const spy = mockFetch(state)
|
||||
const result = await api.auth.me()
|
||||
const [url] = spy.mock.calls[0]
|
||||
expect(url).toBe("/api/v1/auth/me")
|
||||
expect(result).toEqual(state)
|
||||
})
|
||||
})
|
||||
|
||||
it("resource methods hit project-scoped path with projectId first", async () => {
|
||||
const spy = mockFetch([{ id: "a1", provider: "selectel", comment: "" }])
|
||||
const accounts = await api.listAccounts()
|
||||
const accounts = await api.listAccounts(PROJECT_ID)
|
||||
expect(accounts).toHaveLength(1)
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
`/api/v1/projects/${DEFAULT_PROJECT_ID}/accounts`,
|
||||
`/api/v1/projects/${PROJECT_ID}/accounts`,
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("listDomains(projectId) hits /api/v1/projects/{projectId}/domains", async () => {
|
||||
const spy = mockFetch([])
|
||||
await api.listDomains(PROJECT_ID)
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
`/api/v1/projects/${PROJECT_ID}/domains`,
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("sends secret on account creation but path has no secret leakage in response typing", async () => {
|
||||
const spy = mockFetch({ id: "a2", provider: "selectel", comment: "prod" })
|
||||
await api.createAccount({ provider: "selectel", secret: "TOKEN", comment: "prod" })
|
||||
await api.createAccount(PROJECT_ID, { provider: "selectel", secret: "TOKEN", comment: "prod" })
|
||||
const [, opts] = spy.mock.calls[0]
|
||||
expect((opts as RequestInit).method).toBe("POST")
|
||||
expect(String((opts as RequestInit).body)).toContain("TOKEN")
|
||||
@@ -33,14 +87,45 @@ describe("api client", () => {
|
||||
|
||||
it("throws on non-ok response", async () => {
|
||||
mockFetch({ error: "boom" }, false, 500)
|
||||
await expect(api.listDomains()).rejects.toThrow()
|
||||
await expect(api.listDomains(PROJECT_ID)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("applies with prune flag", async () => {
|
||||
it("throws UnauthorizedError on 401", async () => {
|
||||
mockFetch({ error: "unauthorized" }, false, 401)
|
||||
await expect(api.listDomains(PROJECT_ID)).rejects.toThrow(UnauthorizedError)
|
||||
})
|
||||
|
||||
it("applies with prune flag using projectId, id, body order", async () => {
|
||||
const spy = mockFetch({ updates: [], prunes: [], readOnly: [], inSyncCount: 0 })
|
||||
await api.applyDomain("d1", { applyUpdates: true, applyPrunes: true })
|
||||
await api.applyDomain(PROJECT_ID, "d1", { applyUpdates: true, applyPrunes: true })
|
||||
const [url, opts] = spy.mock.calls[0]
|
||||
expect(url).toContain("/domains/d1/apply")
|
||||
expect(url).toBe(`/api/v1/projects/${PROJECT_ID}/domains/d1/apply`)
|
||||
expect(String((opts as RequestInit).body)).toContain("applyPrunes")
|
||||
})
|
||||
|
||||
it("checkDomain(projectId, id) hits project-scoped check path", async () => {
|
||||
const spy = mockFetch({ updates: [], prunes: [], readOnly: [], inSyncCount: 0 })
|
||||
await api.checkDomain(PROJECT_ID, "d1")
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
`/api/v1/projects/${PROJECT_ID}/domains/d1/check`,
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("importZones(projectId, accountId) hits project-scoped import path", async () => {
|
||||
const spy = mockFetch([])
|
||||
await api.importZones(PROJECT_ID, "acc1")
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
`/api/v1/projects/${PROJECT_ID}/accounts/acc1/import`,
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("setDomainTemplate(projectId, id, templateId) hits project-scoped domain path", async () => {
|
||||
const spy = mockFetch({ id: "d1", providerAccountId: "acc1", zoneName: "x.", zoneId: "z1" })
|
||||
await api.setDomainTemplate(PROJECT_ID, "d1", "t1")
|
||||
const [url, opts] = spy.mock.calls[0]
|
||||
expect(url).toBe(`/api/v1/projects/${PROJECT_ID}/domains/d1`)
|
||||
expect((opts as RequestInit).method).toBe("PATCH")
|
||||
})
|
||||
})
|
||||
|
||||
+60
-27
@@ -1,15 +1,25 @@
|
||||
import { API_BASE } from "@/lib/config"
|
||||
import { API_ROOT } from "@/lib/config"
|
||||
import type {
|
||||
AuthState,
|
||||
Account, CreateAccountInput, Template, CreateTemplateInput,
|
||||
Domain, CreateDomainInput, ChangesetResponse, ApplyRequest,
|
||||
} from "./types"
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor() {
|
||||
super("Unauthorized")
|
||||
this.name = "UnauthorizedError"
|
||||
}
|
||||
}
|
||||
|
||||
async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
const res = await fetch(path, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
...init,
|
||||
})
|
||||
if (res.status === 401) throw new UnauthorizedError()
|
||||
if (!res.ok) {
|
||||
let msg = `HTTP ${res.status}`
|
||||
try { const b = await res.json(); if (b?.error) msg = String(b.error) } catch { /* ignore */ }
|
||||
@@ -19,29 +29,52 @@ async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return (await res.json()) as T
|
||||
}
|
||||
|
||||
export const api = {
|
||||
listAccounts: () => req<Account[]>("/accounts"),
|
||||
createAccount: (input: CreateAccountInput) =>
|
||||
req<Account>("/accounts", { method: "POST", body: JSON.stringify(input) }),
|
||||
deleteAccount: (id: string) => req<void>(`/accounts/${id}`, { method: "DELETE" }),
|
||||
|
||||
listTemplates: () => req<Template[]>("/templates"),
|
||||
createTemplate: (input: CreateTemplateInput) =>
|
||||
req<Template>("/templates", { method: "POST", body: JSON.stringify(input) }),
|
||||
updateTemplate: (id: string, input: CreateTemplateInput) =>
|
||||
req<Template>(`/templates/${id}`, { method: "PUT", body: JSON.stringify(input) }),
|
||||
deleteTemplate: (id: string) => req<void>(`/templates/${id}`, { method: "DELETE" }),
|
||||
|
||||
listDomains: () => req<Domain[]>("/domains"),
|
||||
createDomain: (input: CreateDomainInput) =>
|
||||
req<Domain>("/domains", { method: "POST", body: JSON.stringify(input) }),
|
||||
deleteDomain: (id: string) => req<void>(`/domains/${id}`, { method: "DELETE" }),
|
||||
importZones: (accountId: string) =>
|
||||
req<Domain[]>(`/accounts/${accountId}/import`, { method: "POST" }),
|
||||
setDomainTemplate: (id: string, templateId: string | null) =>
|
||||
req<Domain>(`/domains/${id}`, { method: "PATCH", body: JSON.stringify({ templateId }) }),
|
||||
|
||||
checkDomain: (id: string) => req<ChangesetResponse>(`/domains/${id}/check`),
|
||||
applyDomain: (id: string, body: ApplyRequest) =>
|
||||
req<ChangesetResponse>(`/domains/${id}/apply`, { method: "POST", body: JSON.stringify(body) }),
|
||||
function projectPath(projectId: string, path: string): string {
|
||||
return `${API_ROOT}/projects/${projectId}${path}`
|
||||
}
|
||||
|
||||
export const api = {
|
||||
auth: {
|
||||
register: (email: string, password: string) =>
|
||||
req<AuthState>(`${API_ROOT}/auth/register`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
}),
|
||||
login: (email: string, password: string) =>
|
||||
req<AuthState>(`${API_ROOT}/auth/login`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
}),
|
||||
logout: () => req<void>(`${API_ROOT}/auth/logout`, { method: "POST" }),
|
||||
me: () => req<AuthState>(`${API_ROOT}/auth/me`),
|
||||
},
|
||||
|
||||
listAccounts: (projectId: string) => req<Account[]>(projectPath(projectId, "/accounts")),
|
||||
createAccount: (projectId: string, input: CreateAccountInput) =>
|
||||
req<Account>(projectPath(projectId, "/accounts"), { method: "POST", body: JSON.stringify(input) }),
|
||||
deleteAccount: (projectId: string, id: string) =>
|
||||
req<void>(projectPath(projectId, `/accounts/${id}`), { method: "DELETE" }),
|
||||
|
||||
listTemplates: (projectId: string) => req<Template[]>(projectPath(projectId, "/templates")),
|
||||
createTemplate: (projectId: string, input: CreateTemplateInput) =>
|
||||
req<Template>(projectPath(projectId, "/templates"), { method: "POST", body: JSON.stringify(input) }),
|
||||
updateTemplate: (projectId: string, id: string, input: CreateTemplateInput) =>
|
||||
req<Template>(projectPath(projectId, `/templates/${id}`), { method: "PUT", body: JSON.stringify(input) }),
|
||||
deleteTemplate: (projectId: string, id: string) =>
|
||||
req<void>(projectPath(projectId, `/templates/${id}`), { method: "DELETE" }),
|
||||
|
||||
listDomains: (projectId: string) => req<Domain[]>(projectPath(projectId, "/domains")),
|
||||
createDomain: (projectId: string, input: CreateDomainInput) =>
|
||||
req<Domain>(projectPath(projectId, "/domains"), { method: "POST", body: JSON.stringify(input) }),
|
||||
deleteDomain: (projectId: string, id: string) =>
|
||||
req<void>(projectPath(projectId, `/domains/${id}`), { method: "DELETE" }),
|
||||
importZones: (projectId: string, accountId: string) =>
|
||||
req<Domain[]>(projectPath(projectId, `/accounts/${accountId}/import`), { method: "POST" }),
|
||||
setDomainTemplate: (projectId: string, id: string, templateId: string | null) =>
|
||||
req<Domain>(projectPath(projectId, `/domains/${id}`), { method: "PATCH", body: JSON.stringify({ templateId }) }),
|
||||
|
||||
checkDomain: (projectId: string, id: string) =>
|
||||
req<ChangesetResponse>(projectPath(projectId, `/domains/${id}/check`)),
|
||||
applyDomain: (projectId: string, id: string, body: ApplyRequest) =>
|
||||
req<ChangesetResponse>(projectPath(projectId, `/domains/${id}/apply`), { method: "POST", body: JSON.stringify(body) }),
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export interface User { id: string; email: string }
|
||||
export interface Project { id: string; name: string }
|
||||
export interface AuthState { user: User; project: Project }
|
||||
|
||||
export interface Account { id: string; provider: string; comment: string }
|
||||
export interface CreateAccountInput { provider: string; secret: string; comment: string }
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { AuthProvider, useAuth } from "./AuthContext"
|
||||
import { api, UnauthorizedError } from "@/api/client"
|
||||
|
||||
const USER = { id: "u1", email: "a@b.com" }
|
||||
const PROJECT = { id: "p1", name: "Default" }
|
||||
|
||||
function Probe() {
|
||||
const { user, project, loading, login, register, logout } = useAuth()
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="loading">{String(loading)}</span>
|
||||
<span data-testid="user">{user ? user.email : "none"}</span>
|
||||
<span data-testid="project">{project ? project.name : "none"}</span>
|
||||
<button onClick={() => login("a@b.com", "secret")}>login</button>
|
||||
<button onClick={() => register("a@b.com", "secret")}>register</button>
|
||||
<button onClick={() => logout()}>logout</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderProbe() {
|
||||
return render(
|
||||
<AuthProvider>
|
||||
<Probe />
|
||||
</AuthProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe("AuthContext", () => {
|
||||
it("populates user/project from api.auth.me() on mount", async () => {
|
||||
vi.spyOn(api.auth, "me").mockResolvedValue({ user: USER, project: PROJECT })
|
||||
renderProbe()
|
||||
|
||||
expect(screen.getByTestId("loading").textContent).toBe("true")
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId("loading").textContent).toBe("false"))
|
||||
expect(screen.getByTestId("user").textContent).toBe(USER.email)
|
||||
expect(screen.getByTestId("project").textContent).toBe(PROJECT.name)
|
||||
})
|
||||
|
||||
it("treats 401 from api.auth.me() as unauthenticated, not an error", async () => {
|
||||
vi.spyOn(api.auth, "me").mockRejectedValue(new UnauthorizedError())
|
||||
renderProbe()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId("loading").textContent).toBe("false"))
|
||||
expect(screen.getByTestId("user").textContent).toBe("none")
|
||||
expect(screen.getByTestId("project").textContent).toBe("none")
|
||||
})
|
||||
|
||||
it("login sets user/project in context", async () => {
|
||||
vi.spyOn(api.auth, "me").mockRejectedValue(new UnauthorizedError())
|
||||
vi.spyOn(api.auth, "login").mockResolvedValue({ user: USER, project: PROJECT })
|
||||
const user = userEvent.setup()
|
||||
renderProbe()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId("loading").textContent).toBe("false"))
|
||||
await user.click(screen.getByRole("button", { name: "login" }))
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId("user").textContent).toBe(USER.email))
|
||||
expect(screen.getByTestId("project").textContent).toBe(PROJECT.name)
|
||||
})
|
||||
|
||||
it("logout clears user/project from context", async () => {
|
||||
vi.spyOn(api.auth, "me").mockResolvedValue({ user: USER, project: PROJECT })
|
||||
vi.spyOn(api.auth, "logout").mockResolvedValue(undefined)
|
||||
const user = userEvent.setup()
|
||||
renderProbe()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId("user").textContent).toBe(USER.email))
|
||||
await user.click(screen.getByRole("button", { name: "logout" }))
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId("user").textContent).toBe("none"))
|
||||
expect(screen.getByTestId("project").textContent).toBe("none")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react"
|
||||
import { api } from "@/api/client"
|
||||
import type { User, Project } from "@/api/types"
|
||||
|
||||
interface AuthContextValue {
|
||||
user: User | null
|
||||
project: Project | null
|
||||
loading: boolean
|
||||
login: (email: string, password: string) => Promise<void>
|
||||
register: (email: string, password: string) => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [project, setProject] = useState<Project | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
api.auth
|
||||
.me()
|
||||
.then((state) => {
|
||||
if (cancelled) return
|
||||
setUser(state.user)
|
||||
setProject(state.project)
|
||||
})
|
||||
.catch(() => {
|
||||
// Unauthenticated (401) or any other failure — treat as logged-out,
|
||||
// not as a fatal error. Redirect handling is out of scope here (Task 6).
|
||||
if (cancelled) return
|
||||
setUser(null)
|
||||
setProject(null)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const login = useCallback(async (email: string, password: string) => {
|
||||
const state = await api.auth.login(email, password)
|
||||
setUser(state.user)
|
||||
setProject(state.project)
|
||||
}, [])
|
||||
|
||||
const register = useCallback(async (email: string, password: string) => {
|
||||
const state = await api.auth.register(email, password)
|
||||
setUser(state.user)
|
||||
setProject(state.project)
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await api.auth.logout()
|
||||
setUser(null)
|
||||
setProject(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, project, loading, login, register, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const ctx = useContext(AuthContext)
|
||||
if (!ctx) throw new Error("useAuth must be used within an AuthProvider")
|
||||
return ctx
|
||||
}
|
||||
+58
-26
@@ -1,81 +1,113 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { api } from "@/api/client"
|
||||
import { useAuth } from "@/auth/AuthContext"
|
||||
import type { CreateAccountInput, CreateTemplateInput, ApplyRequest } from "@/api/types"
|
||||
|
||||
export function useAccounts() {
|
||||
return useQuery({ queryKey: ["accounts"], queryFn: api.listAccounts })
|
||||
const { project } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ["accounts", project?.id],
|
||||
queryFn: () => api.listAccounts(project!.id),
|
||||
enabled: !!project,
|
||||
})
|
||||
}
|
||||
export function useCreateAccount() {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (input: CreateAccountInput) => api.createAccount(input),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["accounts"] }),
|
||||
mutationFn: (input: CreateAccountInput) => api.createAccount(project!.id, input),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["accounts", project?.id] }),
|
||||
})
|
||||
}
|
||||
export function useDeleteAccount() {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteAccount(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["accounts"] }),
|
||||
mutationFn: (id: string) => api.deleteAccount(project!.id, id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["accounts", project?.id] }),
|
||||
})
|
||||
}
|
||||
|
||||
export function useTemplates() {
|
||||
return useQuery({ queryKey: ["templates"], queryFn: api.listTemplates })
|
||||
const { project } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ["templates", project?.id],
|
||||
queryFn: () => api.listTemplates(project!.id),
|
||||
enabled: !!project,
|
||||
})
|
||||
}
|
||||
export function useCreateTemplate() {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (input: CreateTemplateInput) => api.createTemplate(input),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["templates"] }),
|
||||
mutationFn: (input: CreateTemplateInput) => api.createTemplate(project!.id, input),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["templates", project?.id] }),
|
||||
})
|
||||
}
|
||||
export function useUpdateTemplate() {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, input }: { id: string; input: CreateTemplateInput }) => api.updateTemplate(id, input),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["templates"] }),
|
||||
mutationFn: ({ id, input }: { id: string; input: CreateTemplateInput }) =>
|
||||
api.updateTemplate(project!.id, id, input),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["templates", project?.id] }),
|
||||
})
|
||||
}
|
||||
export function useDeleteTemplate() {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteTemplate(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["templates"] }),
|
||||
mutationFn: (id: string) => api.deleteTemplate(project!.id, id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["templates", project?.id] }),
|
||||
})
|
||||
}
|
||||
|
||||
export function useDomains() {
|
||||
return useQuery({ queryKey: ["domains"], queryFn: api.listDomains })
|
||||
const { project } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ["domains", project?.id],
|
||||
queryFn: () => api.listDomains(project!.id),
|
||||
enabled: !!project,
|
||||
})
|
||||
}
|
||||
export function useImportZones() {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (accountId: string) => api.importZones(accountId),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains"] }),
|
||||
mutationFn: (accountId: string) => api.importZones(project!.id, accountId),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains", project?.id] }),
|
||||
})
|
||||
}
|
||||
export function useSetDomainTemplate() {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, templateId }: { id: string; templateId: string | null }) => api.setDomainTemplate(id, templateId),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains"] }),
|
||||
mutationFn: ({ id, templateId }: { id: string; templateId: string | null }) =>
|
||||
api.setDomainTemplate(project!.id, id, templateId),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains", project?.id] }),
|
||||
})
|
||||
}
|
||||
export function useDeleteDomain() {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.deleteDomain(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains"] }),
|
||||
mutationFn: (id: string) => api.deleteDomain(project!.id, id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["domains", project?.id] }),
|
||||
})
|
||||
}
|
||||
export function useCheckDomain(id: string) {
|
||||
return useQuery({ queryKey: ["check", id], queryFn: () => api.checkDomain(id), enabled: !!id })
|
||||
}
|
||||
export function useApplyDomain(id: string) {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (body: ApplyRequest) => api.applyDomain(id, body),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["check", id] }),
|
||||
const { project } = useAuth()
|
||||
return useQuery({
|
||||
queryKey: ["check", project?.id, id],
|
||||
queryFn: () => api.checkDomain(project!.id, id),
|
||||
enabled: !!project && !!id,
|
||||
})
|
||||
}
|
||||
export function useApplyDomain(id: string) {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (body: ApplyRequest) => api.applyDomain(project!.id, id, body),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["check", project?.id, id] }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const DEFAULT_PROJECT_ID = "00000000-0000-0000-0000-000000000002"
|
||||
export const API_BASE = `/api/v1/projects/${DEFAULT_PROJECT_ID}`
|
||||
export const API_ROOT = "/api/v1"
|
||||
|
||||
Reference in New Issue
Block a user