index.tsx163 lines · main
1import { useState } from 'react'
2import { Badge, Card, CardContent, CardHeader, CardTitle, cn } from 'ui'
3
4import { AnonIcon, AuthenticatedIcon, ServiceRoleIcon } from './Icons'
5import { RoleImpersonationRadio } from './RoleImpersonationRadio'
6import { UserImpersonationSelector } from './UserImpersonationSelector'
7import { DocsButton } from '@/components/ui/DocsButton'
8import { DOCS_URL } from '@/lib/constants'
9import { PostgrestRole } from '@/lib/role-impersonation'
10import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
11
12export interface RoleImpersonationSelectorProps {
13 header?: string
14 serviceRoleLabel?: string
15 disallowAuthenticatedOption?: boolean
16 title?: string
17 orientation?: 'horizontal' | 'vertical'
18}
19
20export const RoleImpersonationSelector = ({
21 header = 'Impersonate a database role',
22 serviceRoleLabel = 'Postgres',
23 disallowAuthenticatedOption = false,
24 orientation = 'horizontal',
25}: RoleImpersonationSelectorProps) => {
26 const isVertical = orientation === 'vertical'
27 const state = useRoleImpersonationStateSnapshot()
28
29 const [selectedOption, setSelectedOption] = useState<PostgrestRole | undefined>(() => {
30 if (
31 state.role?.type === 'postgrest' &&
32 (state.role.role === 'anon' || state.role.role === 'authenticated')
33 ) {
34 return state.role.role
35 }
36
37 return 'service_role'
38 })
39
40 const isAuthenticatedOptionFullySelected = Boolean(
41 selectedOption === 'authenticated' &&
42 state.role?.type === 'postgrest' &&
43 state.role.role === 'authenticated' &&
44 (('user' in state.role && state.role.user) ||
45 ('externalAuth' in state.role && state.role.externalAuth)) // Check for either auth type
46 )
47
48 function onSelectedChange(value: PostgrestRole) {
49 if (value === 'service_role') {
50 // do not set a role for service role
51 // as the default role is the "service role"
52 state.setRole(undefined)
53 }
54
55 if (value === 'anon') {
56 state.setRole({
57 type: 'postgrest',
58 role: value,
59 })
60 }
61
62 setSelectedOption(value)
63 }
64
65 return (
66 <Card className="border-none">
67 <CardHeader className="flex-row items-center justify-between py-3 space-y-0">
68 <CardTitle>{header}</CardTitle>
69 <DocsButton
70 href={`${DOCS_URL}/guides/database/postgres/row-level-security#authenticated-and-unauthenticated-roles`}
71 />
72 </CardHeader>
73 <CardContent className="flex flex-col gap-y-4">
74 <form
75 onSubmit={(e) => {
76 // don't allow form submission
77 e.preventDefault()
78 }}
79 >
80 <fieldset className={cn('flex gap-3', isVertical && 'flex-col gap-2')}>
81 <RoleImpersonationRadio
82 value="service_role"
83 isSelected={selectedOption === 'service_role'}
84 onSelectedChange={onSelectedChange}
85 label={serviceRoleLabel}
86 description="Superuser"
87 icon={<ServiceRoleIcon isSelected={selectedOption === 'service_role'} />}
88 fullWidth={isVertical}
89 />
90
91 <RoleImpersonationRadio
92 value="anon"
93 label="Anonymous"
94 isSelected={selectedOption === 'anon'}
95 onSelectedChange={onSelectedChange}
96 description="Not logged in"
97 icon={<AnonIcon isSelected={selectedOption === 'anon'} />}
98 fullWidth={isVertical}
99 />
100
101 {!disallowAuthenticatedOption && (
102 <RoleImpersonationRadio
103 value="authenticated"
104 label="Authenticated"
105 isSelected={
106 selectedOption === 'authenticated' &&
107 (isAuthenticatedOptionFullySelected || 'partially')
108 }
109 onSelectedChange={onSelectedChange}
110 description="Specific logged in user"
111 icon={<AuthenticatedIcon isSelected={selectedOption === 'authenticated'} />}
112 fullWidth={isVertical}
113 />
114 )}
115 </fieldset>
116 </form>
117
118 {selectedOption === 'service_role' && (
119 <div>
120 <p className="text-sm">
121 Full admin access
122 <Badge className="ml-2">Default</Badge>
123 </p>
124 <p className="text-foreground-light text-sm">
125 The <code className="text-code-inline">postgres</code> role, which bypasses all Row
126 Level Security (RLS) policies.
127 </p>
128 </div>
129 )}
130
131 {selectedOption === 'anon' && (
132 <div>
133 <p className="text-sm">For unauthenticated access</p>
134 <p className="text-foreground-light text-sm">
135 The <code className="text-code-inline">anon</code> role, which the API (PostgREST)
136 uses when a user is not logged in.
137 <br />
138 Row Level Security (RLS) policies apply.
139 </p>
140 </div>
141 )}
142
143 {selectedOption === 'authenticated' && (
144 <div>
145 <p className="text-sm">For authenticated access</p>
146 <p className="text-foreground-light text-sm">
147 The <code className="text-code-inline">authenticated</code> role, which the API
148 (PostgREST) uses when a user is logged in.
149 <br />
150 Row Level Security (RLS) policies apply.
151 </p>
152 </div>
153 )}
154 </CardContent>
155
156 {selectedOption === 'authenticated' && (
157 <CardContent className="p-0">
158 <UserImpersonationSelector />
159 </CardContent>
160 )}
161 </Card>
162 )
163}