auth.tsx369 lines · main
1import { useParams } from 'common'
2import dayjs from 'dayjs'
3import { ArrowRight, LogsIcon, RefreshCw } from 'lucide-react'
4import { useRouter } from 'next/router'
5import { parseAsJson, useQueryState } from 'nuqs'
6import { useState } from 'react'
7
8import ReportFilterBar from '@/components/interfaces/Reports/ReportFilterBar'
9import ReportHeader from '@/components/interfaces/Reports/ReportHeader'
10import ReportPadding from '@/components/interfaces/Reports/ReportPadding'
11import { REPORT_DATERANGE_HELPER_LABELS } from '@/components/interfaces/Reports/Reports.constants'
12import ReportStickyNav from '@/components/interfaces/Reports/ReportStickyNav'
13import { SharedAPIReport } from '@/components/interfaces/Reports/SharedAPIReport/SharedAPIReport'
14import { useSharedAPIReport } from '@/components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants'
15import { ReportChartV2 } from '@/components/interfaces/Reports/v2/ReportChartV2'
16import { ReportSectionHeader } from '@/components/interfaces/Reports/v2/ReportSectionHeader'
17import {
18 numericFilterSchema,
19 ReportsNumericFilter,
20} from '@/components/interfaces/Reports/v2/ReportsNumericFilter'
21import {
22 ReportsSelectFilter,
23 selectFilterSchema,
24} from '@/components/interfaces/Reports/v2/ReportsSelectFilter'
25import { LogsDatePicker } from '@/components/interfaces/Settings/Logs/Logs.DatePickers'
26import UpgradePrompt from '@/components/interfaces/Settings/Logs/UpgradePrompt'
27import DefaultLayout from '@/components/layouts/DefaultLayout'
28import ObservabilityLayout from '@/components/layouts/ObservabilityLayout/ObservabilityLayout'
29import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
30import type { ChartHighlightAction } from '@/components/ui/Charts/ChartHighlightActions'
31import { ReportSettings } from '@/components/ui/Charts/ReportSettings'
32import { ObservabilityLink } from '@/components/ui/ObservabilityLink'
33import {
34 createErrorsReportConfig,
35 createLatencyReportConfig,
36 createUsageReportConfig,
37} from '@/data/reports/v2/auth.config'
38import { useRefreshHandler, useReportDateRange } from '@/hooks/misc/useReportDateRange'
39import type { NextPageWithLayout } from '@/types'
40
41const AuthReport: NextPageWithLayout = () => {
42 return (
43 <ReportPadding>
44 <AuthUsage />
45 </ReportPadding>
46 )
47}
48
49AuthReport.getLayout = (page) => (
50 <DefaultLayout>
51 <ObservabilityLayout title="Auth">{page}</ObservabilityLayout>
52 </DefaultLayout>
53)
54
55export type UpdateDateRange = (from: string, to: string) => void
56export default AuthReport
57
58const AuthUsage = () => {
59 const { ref } = useParams()
60 const chartSyncId = `auth-report`
61
62 const {
63 selectedDateRange,
64 updateDateRange,
65 datePickerValue,
66 datePickerHelpers,
67 showUpgradePrompt,
68 setShowUpgradePrompt,
69 handleDatePickerChange,
70 } = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES)
71
72 const {
73 data,
74 error,
75 isLoading,
76 refetch,
77 isRefetching,
78 filters,
79 addFilter,
80 removeFilters,
81 isLoadingData,
82 sql,
83 } = useSharedAPIReport({
84 filterBy: 'auth',
85 start: selectedDateRange?.period_start?.date,
86 end: selectedDateRange?.period_end?.date,
87 })
88
89 const [isRefreshing, setIsRefreshing] = useState(false)
90
91 const [monitoringStatusCodeFilter, setMonitoringStatusCodeFilter] = useQueryState(
92 'monitoring_status_code',
93 parseAsJson(numericFilterSchema.parse)
94 )
95
96 const [usageProviderFilter, setUsageProviderFilter] = useQueryState(
97 'usage_provider',
98 parseAsJson(selectFilterSchema.parse)
99 )
100
101 const providerOptions = [
102 { label: 'Email', value: 'email' },
103 { label: 'Google', value: 'google' },
104 { label: 'Apple', value: 'apple' },
105 { label: 'Phone', value: 'phone' },
106 { label: 'Discord', value: 'discord' },
107 { label: 'Azure', value: 'azure' },
108 { label: 'GitHub', value: 'github' },
109 { label: 'Kakao', value: 'kakao' },
110 { label: 'TOTP', value: 'totp' },
111 { label: 'Twitter', value: 'twitter' },
112 { label: 'SAML', value: 'saml' },
113 { label: 'Recovery', value: 'recovery' },
114 { label: 'SSO/SAML', value: 'sso/saml' },
115 { label: 'Magic Link', value: 'magiclink' },
116 { label: 'Keycloak', value: 'keycloak' },
117 { label: 'Facebook', value: 'facebook' },
118 { label: 'Twitch', value: 'twitch' },
119 { label: 'LinkedIn OIDC', value: 'linkedin_oidc' },
120 { label: 'Spotify', value: 'spotify' },
121 { label: 'Email Change', value: 'email_change' },
122 { label: 'Snapchat', value: 'snapchat' },
123 { label: 'WorkOS', value: 'workos' },
124 { label: 'Notion', value: 'notion' },
125 { label: 'Slack OIDC', value: 'slack_oidc' },
126 { label: 'Figma', value: 'figma' },
127 { label: 'GitLab', value: 'gitlab' },
128 { label: 'LinkedIn', value: 'linkedin' },
129 { label: 'Zoom', value: 'zoom' },
130 ]
131
132 const usageReportConfig = createUsageReportConfig({
133 projectRef: ref || '',
134 startDate: selectedDateRange?.period_start?.date,
135 endDate: selectedDateRange?.period_end?.date,
136 interval: selectedDateRange?.interval,
137 filters: { provider: usageProviderFilter },
138 })
139
140 const errorsReportConfig = createErrorsReportConfig({
141 projectRef: ref || '',
142 startDate: selectedDateRange?.period_start?.date,
143 endDate: selectedDateRange?.period_end?.date,
144 interval: selectedDateRange?.interval,
145 filters: { status_code: monitoringStatusCodeFilter },
146 })
147
148 const latencyReportConfig = createLatencyReportConfig({
149 projectRef: ref || '',
150 startDate: selectedDateRange?.period_start?.date,
151 endDate: selectedDateRange?.period_end?.date,
152 interval: selectedDateRange?.interval,
153 filters: {},
154 })
155
156 const onRefreshReport = useRefreshHandler(
157 datePickerValue,
158 datePickerHelpers,
159 handleDatePickerChange,
160 async () => {
161 if (!selectedDateRange) return
162
163 setIsRefreshing(true)
164 refetch()
165 setTimeout(() => setIsRefreshing(false), 1000)
166 }
167 )
168
169 const router = useRouter()
170
171 const highlightActions: ChartHighlightAction[] = [
172 {
173 id: 'api-gateway-logs',
174 label: 'Open in API Gateway Logs',
175 icon: <LogsIcon size={12} />,
176 onSelect: ({ start, end, chartId }) => {
177 let url = `/project/${ref}/logs/edge-logs?its=${start}&ite=${end}`
178
179 if (chartId?.includes('errors')) {
180 url += `&f={"product":{"auth":true},"status_code":{"error":true,"warning":true}}`
181 } else {
182 url += `&f={"product":{"auth":true}}`
183 }
184
185 router.push(url)
186 },
187 },
188 {
189 id: 'auth-logs',
190 label: 'Open in Auth Logs',
191 icon: <LogsIcon size={12} />,
192 onSelect: ({ start, end }) => {
193 const url = `/project/${ref}/logs/auth-logs?its=${start}&ite=${end}`
194 router.push(url)
195 },
196 },
197 ]
198
199 return (
200 <>
201 <ReportHeader title="Auth" showDatabaseSelector={false} />
202 <ReportStickyNav
203 content={
204 <div className="flex flex-col gap-2">
205 <div className="flex items-center gap-2">
206 <ButtonTooltip
207 type="default"
208 disabled={isRefreshing}
209 icon={<RefreshCw className={isRefreshing ? 'animate-spin' : ''} />}
210 className="w-7"
211 tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }}
212 onClick={onRefreshReport}
213 />
214 <ReportSettings chartId={chartSyncId} />
215 <LogsDatePicker
216 onSubmit={handleDatePickerChange}
217 value={datePickerValue}
218 helpers={datePickerHelpers}
219 />
220 <UpgradePrompt
221 show={showUpgradePrompt}
222 setShowUpgradePrompt={setShowUpgradePrompt}
223 title="Report date range"
224 description="Report data can be stored for a maximum of 3 months depending on the plan that your project is on."
225 source="authReportDateRange"
226 />
227 {selectedDateRange && (
228 <div className="flex items-center gap-x-2 text-xs">
229 <p className="text-foreground-light">
230 {dayjs(selectedDateRange.period_start.date).format('MMM D, h:mma')}
231 </p>
232 <p className="text-foreground-light">
233 <ArrowRight size={12} />
234 </p>
235 <p className="text-foreground-light">
236 {dayjs(selectedDateRange.period_end.date).format('MMM D, h:mma')}
237 </p>
238 </div>
239 )}
240 </div>
241 </div>
242 }
243 >
244 <div className="mt-8 flex flex-col gap-8 pb-8">
245 <div className="flex flex-col gap-4" id="usage">
246 <div>
247 <ReportSectionHeader
248 id="usage"
249 title="Usage"
250 description="Monitor user activity, sign-ins, sign-ups, and password reset requests to understand how users interact with your authentication system."
251 />
252 <ReportsSelectFilter
253 label="Provider"
254 options={providerOptions}
255 value={usageProviderFilter || []}
256 onChange={setUsageProviderFilter}
257 isLoading={isRefreshing}
258 showSearch={true}
259 />
260 </div>
261 <div className="grid md:grid-cols-2 gap-4">
262 {usageReportConfig.map((metric) => (
263 <ReportChartV2
264 key={`${metric.id}`}
265 report={metric}
266 projectRef={ref!}
267 interval={selectedDateRange.interval}
268 startDate={selectedDateRange?.period_start?.date}
269 endDate={selectedDateRange?.period_end?.date}
270 updateDateRange={updateDateRange}
271 syncId={chartSyncId}
272 filters={{ provider: usageProviderFilter }}
273 highlightActions={highlightActions}
274 />
275 ))}
276 </div>
277 </div>
278
279 <div className="flex flex-col gap-4" id="monitoring">
280 <div>
281 <ReportSectionHeader
282 id="monitoring"
283 title="Monitoring"
284 description="Track authentication errors by status code and error type to identify issues and improve user experience."
285 />
286
287 <ReportsNumericFilter
288 label="Status Code"
289 value={monitoringStatusCodeFilter}
290 onChange={setMonitoringStatusCodeFilter}
291 defaultOperator="="
292 isLoading={isRefreshing}
293 />
294 </div>
295 <div className="grid md:grid-cols-2 gap-4">
296 {errorsReportConfig.map((metric) => (
297 <ReportChartV2
298 key={`${metric.id}`}
299 report={metric}
300 projectRef={ref!}
301 interval={selectedDateRange.interval}
302 startDate={selectedDateRange?.period_start?.date}
303 endDate={selectedDateRange?.period_end?.date}
304 updateDateRange={updateDateRange}
305 syncId={chartSyncId}
306 filters={{ status_code: monitoringStatusCodeFilter }}
307 highlightActions={highlightActions}
308 />
309 ))}
310 </div>
311 </div>
312
313 <div className="flex flex-col gap-4" id="performance">
314 <ReportSectionHeader
315 id="performance"
316 title="Performance"
317 description="Monitor sign-in and sign-up performance metrics including average, percentiles, and request counts to ensure optimal authentication speed."
318 />
319 <div className="grid md:grid-cols-2 gap-4">
320 {latencyReportConfig.map((metric) => (
321 <ReportChartV2
322 key={`${metric.id}`}
323 report={metric}
324 projectRef={ref!}
325 interval={selectedDateRange.interval}
326 startDate={selectedDateRange?.period_start?.date}
327 endDate={selectedDateRange?.period_end?.date}
328 updateDateRange={updateDateRange}
329 syncId={chartSyncId}
330 filters={{}}
331 highlightActions={highlightActions}
332 />
333 ))}
334 </div>
335 </div>
336 <div>
337 <div className="mb-4 space-y-4">
338 <ReportSectionHeader
339 id="auth-api-gateway"
340 title="Auth API Gateway"
341 description="Monitor user activity, sign-ins, sign-ups, and password reset requests to understand how users interact with your authentication system."
342 />
343 <ReportFilterBar
344 filters={filters}
345 onAddFilter={addFilter}
346 onRemoveFilters={removeFilters}
347 isLoading={isLoadingData || isRefetching}
348 hideDatepicker={true}
349 datepickerHelpers={datePickerHelpers}
350 selectedProduct={'auth'}
351 showDatabaseSelector={false}
352 />
353 </div>
354 <SharedAPIReport
355 data={data}
356 error={error}
357 isLoading={isLoading}
358 isRefetching={isRefetching}
359 sql={sql}
360 />
361 </div>
362 </div>
363 </ReportStickyNav>
364 <div className="pb-8">
365 <ObservabilityLink />
366 </div>
367 </>
368 )
369}