@q3assets/auth 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -20
- package/package.json +3 -1
- package/src/admin-panel.tsx +187 -0
- package/src/admin-routes.ts +73 -0
- package/src/admin.ts +27 -35
- package/src/index.ts +3 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @q3assets/auth
|
|
2
2
|
|
|
3
|
-
Shared authentication package for SHIRE project dashboards.
|
|
3
|
+
Shared authentication package for SHIRE project dashboards. Published to npm as `@q3assets/auth`.
|
|
4
4
|
|
|
5
5
|
## What's included
|
|
6
6
|
|
|
@@ -17,38 +17,35 @@ Shared authentication package for SHIRE project dashboards. Provides M365-safe m
|
|
|
17
17
|
|
|
18
18
|
## Consuming from a project dashboard
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
**1. Install:**
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```json
|
|
25
|
-
"@q3/auth": "file:../../Q3/packages/auth"
|
|
22
|
+
```bash
|
|
23
|
+
npm install @q3assets/auth
|
|
26
24
|
```
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
**2. Configure `next.config.ts`:**
|
|
26
|
+
**2. Add `transpilePackages` to `next.config.ts`:**
|
|
31
27
|
|
|
32
28
|
```typescript
|
|
33
|
-
import { join } from "path"
|
|
34
|
-
|
|
35
29
|
const nextConfig = {
|
|
36
|
-
transpilePackages: ["@
|
|
37
|
-
turbopack: {
|
|
38
|
-
root: join(__dirname, "../.."), // common parent: SHIRE/projects/
|
|
39
|
-
},
|
|
30
|
+
transpilePackages: ["@q3assets/auth"],
|
|
40
31
|
}
|
|
41
32
|
```
|
|
42
33
|
|
|
43
|
-
|
|
34
|
+
This is required because the package ships TypeScript source. One line, standard Next.js pattern.
|
|
44
35
|
|
|
45
|
-
**3.
|
|
36
|
+
**3. Import:**
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
```typescript
|
|
39
|
+
import { Login } from "@q3assets/auth/login"
|
|
40
|
+
import { AuthConfirm } from "@q3assets/auth/confirm"
|
|
41
|
+
import { AuthCallback } from "@q3assets/auth/callback"
|
|
42
|
+
import { createAuthMiddleware } from "@q3assets/auth/middleware"
|
|
43
|
+
import { createBrowserClient } from "@q3assets/auth/clients"
|
|
44
|
+
```
|
|
48
45
|
|
|
49
46
|
### Pages and middleware
|
|
50
47
|
|
|
51
|
-
Create three pages and a middleware. Reference implementation: `Q3/dashboard/src/`
|
|
48
|
+
Create three pages and a middleware. Reference implementation: `SHIRE/projects/Q3/dashboard/src/`
|
|
52
49
|
|
|
53
50
|
```
|
|
54
51
|
app/login/page.tsx → Login component
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q3assets/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./src/index.ts",
|
|
6
6
|
"./login": "./src/login.tsx",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"./confirm": "./src/confirm.tsx",
|
|
9
9
|
"./clients": "./src/clients.ts",
|
|
10
10
|
"./admin": "./src/admin.ts",
|
|
11
|
+
"./admin-panel": "./src/admin-panel.tsx",
|
|
12
|
+
"./admin-routes": "./src/admin-routes.ts",
|
|
11
13
|
"./middleware": "./src/middleware.ts"
|
|
12
14
|
},
|
|
13
15
|
"dependencies": {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import type { SupabaseClient } from "@supabase/supabase-js"
|
|
5
|
+
|
|
6
|
+
interface AppUser {
|
|
7
|
+
id: string
|
|
8
|
+
email: string
|
|
9
|
+
name: string | null
|
|
10
|
+
role: string
|
|
11
|
+
created_at: string
|
|
12
|
+
last_sign_in_at: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AdminPanelProps {
|
|
16
|
+
supabase: SupabaseClient
|
|
17
|
+
onClose: () => void
|
|
18
|
+
apiBase?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AdminPanel({ supabase, onClose, apiBase = "/api/admin/users" }: AdminPanelProps) {
|
|
22
|
+
const [users, setUsers] = useState<AppUser[]>([])
|
|
23
|
+
const [loading, setLoading] = useState(true)
|
|
24
|
+
const [inviteName, setInviteName] = useState("")
|
|
25
|
+
const [inviteEmail, setInviteEmail] = useState("")
|
|
26
|
+
const [inviteRole, setInviteRole] = useState<"user" | "admin">("user")
|
|
27
|
+
const [sending, setSending] = useState(false)
|
|
28
|
+
const [message, setMessage] = useState("")
|
|
29
|
+
const [error, setError] = useState("")
|
|
30
|
+
const [editingUser, setEditingUser] = useState<AppUser | null>(null)
|
|
31
|
+
const [editName, setEditName] = useState("")
|
|
32
|
+
|
|
33
|
+
async function getAuthHeader() {
|
|
34
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
35
|
+
return { Authorization: `Bearer ${session?.access_token}` }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function loadUsers() {
|
|
39
|
+
const headers = await getAuthHeader()
|
|
40
|
+
const res = await fetch(apiBase, { headers })
|
|
41
|
+
const data = await res.json()
|
|
42
|
+
if (res.ok) {
|
|
43
|
+
setUsers(data.users)
|
|
44
|
+
setError("")
|
|
45
|
+
} else {
|
|
46
|
+
setError(data.error || "Failed to load users")
|
|
47
|
+
}
|
|
48
|
+
setLoading(false)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
useEffect(() => { loadUsers() }, [])
|
|
52
|
+
|
|
53
|
+
async function handleInvite(e: React.FormEvent) {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
setSending(true)
|
|
56
|
+
setMessage("")
|
|
57
|
+
|
|
58
|
+
const headers = await getAuthHeader()
|
|
59
|
+
const res = await fetch(apiBase, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
62
|
+
body: JSON.stringify({ email: inviteEmail, name: inviteName, role: inviteRole }),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const data = await res.json()
|
|
66
|
+
setSending(false)
|
|
67
|
+
|
|
68
|
+
if (res.ok) {
|
|
69
|
+
setMessage(`Invited ${inviteName || inviteEmail}`)
|
|
70
|
+
setInviteEmail("")
|
|
71
|
+
setInviteName("")
|
|
72
|
+
loadUsers()
|
|
73
|
+
} else {
|
|
74
|
+
setMessage(data.error || "Failed to invite")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleRemove(user: AppUser) {
|
|
79
|
+
if (!confirm(`Remove ${user.email}? This cannot be undone.`)) return
|
|
80
|
+
const headers = await getAuthHeader()
|
|
81
|
+
const res = await fetch(`${apiBase}/${user.id}`, { method: "DELETE", headers })
|
|
82
|
+
if (res.ok) loadUsers()
|
|
83
|
+
else alert((await res.json()).error || "Failed to remove user")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleToggleRole(user: AppUser) {
|
|
87
|
+
const newRole = user.role === "admin" ? "user" : "admin"
|
|
88
|
+
const headers = await getAuthHeader()
|
|
89
|
+
const res = await fetch(`${apiBase}/${user.id}`, {
|
|
90
|
+
method: "PATCH",
|
|
91
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ role: newRole }),
|
|
93
|
+
})
|
|
94
|
+
if (res.ok) loadUsers()
|
|
95
|
+
else alert((await res.json()).error || "Failed to update role")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleSaveName(user: AppUser) {
|
|
99
|
+
const headers = await getAuthHeader()
|
|
100
|
+
const res = await fetch(`${apiBase}/${user.id}`, {
|
|
101
|
+
method: "PATCH",
|
|
102
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ name: editName }),
|
|
104
|
+
})
|
|
105
|
+
if (res.ok) { setEditingUser(null); setEditName(""); loadUsers() }
|
|
106
|
+
else alert((await res.json()).error || "Failed to update name")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="fixed inset-0 z-50 flex justify-end">
|
|
111
|
+
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
112
|
+
<div className="relative w-full max-w-lg overflow-y-auto bg-[var(--bg)] border-l border-[var(--border)] shadow-2xl">
|
|
113
|
+
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-[var(--border)] bg-[var(--bg)] px-5 py-4">
|
|
114
|
+
<h2 className="text-lg font-semibold">Admin</h2>
|
|
115
|
+
<button onClick={onClose} className="rounded-lg p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-card)] hover:text-[var(--text)]">
|
|
116
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12" /></svg>
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="p-5 space-y-6">
|
|
121
|
+
<div className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] p-4">
|
|
122
|
+
<h3 className="text-sm font-medium mb-3">Invite User</h3>
|
|
123
|
+
<form onSubmit={handleInvite} className="space-y-3">
|
|
124
|
+
<input type="text" value={inviteName} onChange={(e) => setInviteName(e.target.value)} placeholder="Name (required)" required className="w-full rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm text-[var(--text)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-1 focus:ring-[var(--accent)]" />
|
|
125
|
+
<input type="email" value={inviteEmail} onChange={(e) => setInviteEmail(e.target.value)} placeholder="email@example.com" required className="w-full rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm text-[var(--text)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-1 focus:ring-[var(--accent)]" />
|
|
126
|
+
<div className="flex items-center gap-3">
|
|
127
|
+
<select value={inviteRole} onChange={(e) => setInviteRole(e.target.value as "user" | "admin")} className="rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-sm text-[var(--text)] focus:border-[var(--accent)] focus:outline-none">
|
|
128
|
+
<option value="user">User</option>
|
|
129
|
+
<option value="admin">Admin</option>
|
|
130
|
+
</select>
|
|
131
|
+
<button type="submit" disabled={sending} className="flex-1 rounded-lg bg-[var(--accent)] px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-[var(--accent-light)] disabled:opacity-50">
|
|
132
|
+
{sending ? "Sending..." : "Send Invite"}
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
{message && <p className="text-xs text-[var(--text-muted)]">{message}</p>}
|
|
136
|
+
</form>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div>
|
|
140
|
+
<h3 className="text-sm font-medium mb-3">Users ({users.length})</h3>
|
|
141
|
+
{error && <p className="mb-3 text-sm text-[var(--danger)]">{error}</p>}
|
|
142
|
+
{loading ? (
|
|
143
|
+
<p className="text-sm text-[var(--text-muted)]">Loading...</p>
|
|
144
|
+
) : users.length === 0 ? (
|
|
145
|
+
<p className="text-sm text-[var(--text-muted)]">No users yet.</p>
|
|
146
|
+
) : (
|
|
147
|
+
<div className="space-y-2">
|
|
148
|
+
{users.map((user) => (
|
|
149
|
+
<div key={user.id} className="rounded-lg border border-[var(--border)] bg-[var(--bg-card)] px-4 py-3">
|
|
150
|
+
{editingUser?.id === user.id ? (
|
|
151
|
+
<div className="flex items-center gap-2">
|
|
152
|
+
<input type="text" value={editName} onChange={(e) => setEditName(e.target.value)} placeholder="Name" autoFocus onKeyDown={(e) => { if (e.key === "Enter") handleSaveName(user); if (e.key === "Escape") { setEditingUser(null); setEditName("") } }} className="flex-1 rounded-lg border border-[var(--border)] bg-[var(--bg)] px-3 py-1.5 text-sm text-[var(--text)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-1 focus:ring-[var(--accent)]" />
|
|
153
|
+
<button onClick={() => handleSaveName(user)} className="rounded-lg bg-[var(--accent)] px-3 py-1.5 text-xs font-medium text-white hover:bg-[var(--accent-light)]">Save</button>
|
|
154
|
+
<button onClick={() => { setEditingUser(null); setEditName("") }} className="rounded-lg px-3 py-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text)]">Cancel</button>
|
|
155
|
+
</div>
|
|
156
|
+
) : (
|
|
157
|
+
<div className="flex items-center justify-between">
|
|
158
|
+
<div className="min-w-0 flex-1">
|
|
159
|
+
<button onClick={() => { setEditingUser(user); setEditName(user.name || "") }} className="text-sm font-medium truncate hover:text-[var(--accent)] transition-colors text-left" title="Click to edit name">
|
|
160
|
+
{user.name || user.email}
|
|
161
|
+
</button>
|
|
162
|
+
<p className="text-xs text-[var(--text-muted)] truncate">
|
|
163
|
+
{user.name ? user.email : null}
|
|
164
|
+
{user.name && user.last_sign_in_at ? " · " : ""}
|
|
165
|
+
{user.last_sign_in_at ? `Last login ${new Date(user.last_sign_in_at).toLocaleDateString()}` : "Never logged in"}
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
168
|
+
<div className="flex items-center gap-2 ml-3">
|
|
169
|
+
<button onClick={() => handleToggleRole(user)} className={`rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${user.role === "admin" ? "bg-indigo-900/50 text-indigo-300 hover:bg-indigo-900/70" : "bg-zinc-800 text-zinc-300 hover:bg-zinc-700"}`}>
|
|
170
|
+
{user.role}
|
|
171
|
+
</button>
|
|
172
|
+
<button onClick={() => handleRemove(user)} className="rounded-lg p-1 text-[var(--text-muted)] hover:text-[var(--danger)] transition-colors" title="Remove user">
|
|
173
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14" /></svg>
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createAdminClient, requireAdmin } from "./admin"
|
|
2
|
+
|
|
3
|
+
function json(data: any, status = 200) {
|
|
4
|
+
return new Response(JSON.stringify(data), {
|
|
5
|
+
status,
|
|
6
|
+
headers: { "Content-Type": "application/json" },
|
|
7
|
+
})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function handleListUsers(request: Request) {
|
|
11
|
+
const user = await requireAdmin(request)
|
|
12
|
+
if (!user) return json({ error: "Unauthorized" }, 401)
|
|
13
|
+
|
|
14
|
+
const admin = createAdminClient()
|
|
15
|
+
const { data, error } = await admin.auth.admin.listUsers()
|
|
16
|
+
if (error) return json({ error: error.message }, 500)
|
|
17
|
+
|
|
18
|
+
const users = data.users.map((u) => ({
|
|
19
|
+
id: u.id,
|
|
20
|
+
email: u.email,
|
|
21
|
+
name: u.user_metadata?.name || null,
|
|
22
|
+
role: u.user_metadata?.role || u.app_metadata?.role || "user",
|
|
23
|
+
created_at: u.created_at,
|
|
24
|
+
last_sign_in_at: u.last_sign_in_at,
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
return json({ users })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleInviteUser(request: Request) {
|
|
31
|
+
const user = await requireAdmin(request)
|
|
32
|
+
if (!user) return json({ error: "Unauthorized" }, 401)
|
|
33
|
+
|
|
34
|
+
const { email, name, role } = await request.json()
|
|
35
|
+
if (!email) return json({ error: "Email required" }, 400)
|
|
36
|
+
|
|
37
|
+
const admin = createAdminClient()
|
|
38
|
+
const siteUrl = new URL(request.url).origin
|
|
39
|
+
|
|
40
|
+
const { data, error } = await admin.auth.admin.inviteUserByEmail(email, {
|
|
41
|
+
redirectTo: `${siteUrl}/auth/confirm`,
|
|
42
|
+
data: { name: name || email.split("@")[0], role: role || "user" },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (error) return json({ error: error.message }, 500)
|
|
46
|
+
return json({ user: data.user })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handleUpdateUser(request: Request, userId: string) {
|
|
50
|
+
const user = await requireAdmin(request)
|
|
51
|
+
if (!user) return json({ error: "Unauthorized" }, 401)
|
|
52
|
+
|
|
53
|
+
const body = await request.json()
|
|
54
|
+
const admin = createAdminClient()
|
|
55
|
+
|
|
56
|
+
const update: any = {}
|
|
57
|
+
if (body.role) update.app_metadata = { role: body.role }
|
|
58
|
+
if (body.name !== undefined) update.user_metadata = { name: body.name }
|
|
59
|
+
|
|
60
|
+
const { error } = await admin.auth.admin.updateUserById(userId, update)
|
|
61
|
+
if (error) return json({ error: error.message }, 500)
|
|
62
|
+
return json({ ok: true })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function handleDeleteUser(request: Request, userId: string) {
|
|
66
|
+
const user = await requireAdmin(request)
|
|
67
|
+
if (!user) return json({ error: "Unauthorized" }, 401)
|
|
68
|
+
|
|
69
|
+
const admin = createAdminClient()
|
|
70
|
+
const { error } = await admin.auth.admin.deleteUser(userId)
|
|
71
|
+
if (error) return json({ error: error.message }, 500)
|
|
72
|
+
return json({ ok: true })
|
|
73
|
+
}
|
package/src/admin.ts
CHANGED
|
@@ -1,52 +1,44 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js"
|
|
1
2
|
import type { SupabaseClient } from "@supabase/supabase-js"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
): Promise<
|
|
4
|
+
export function createAdminClient(
|
|
5
|
+
url?: string,
|
|
6
|
+
serviceRoleKey?: string
|
|
7
|
+
): SupabaseClient {
|
|
8
|
+
const u = url || process.env.NEXT_PUBLIC_SUPABASE_URL!
|
|
9
|
+
const k = serviceRoleKey || process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
10
|
+
if (!k) throw new Error("SUPABASE_SERVICE_ROLE_KEY not configured")
|
|
11
|
+
return createClient(u, k, {
|
|
12
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function requireAdmin(request: Request): Promise<any | null> {
|
|
16
17
|
const authHeader = request.headers.get("authorization")
|
|
17
|
-
if (!authHeader)
|
|
18
|
-
return { user: null, error: "Not authenticated" }
|
|
19
|
-
}
|
|
18
|
+
if (!authHeader?.startsWith("Bearer ")) return null
|
|
20
19
|
|
|
21
|
-
const token = authHeader.
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
const token = authHeader.slice(7)
|
|
21
|
+
const supabase = createClient(
|
|
22
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
23
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
24
|
+
)
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
26
|
+
const { data: { user }, error } = await supabase.auth.getUser(token)
|
|
27
|
+
if (error || !user) return null
|
|
30
28
|
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
return { user: null, error: "Forbidden" }
|
|
34
|
-
}
|
|
29
|
+
const role = user.user_metadata?.role || user.app_metadata?.role
|
|
30
|
+
if (role !== "admin" && user.app_metadata?.is_admin !== true) return null
|
|
35
31
|
|
|
36
|
-
return
|
|
32
|
+
return user
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
/**
|
|
40
|
-
* Invite a user by email using the service-role client.
|
|
41
|
-
* Sets redirect to /auth/confirm for M365 safety.
|
|
42
|
-
*/
|
|
43
35
|
export async function inviteUser(
|
|
44
|
-
|
|
36
|
+
adminClient: SupabaseClient,
|
|
45
37
|
email: string,
|
|
46
38
|
siteUrl: string,
|
|
47
39
|
metadata?: Record<string, any>
|
|
48
40
|
) {
|
|
49
|
-
return
|
|
41
|
+
return adminClient.auth.admin.inviteUserByEmail(email, {
|
|
50
42
|
redirectTo: `${siteUrl}/auth/confirm`,
|
|
51
43
|
data: metadata,
|
|
52
44
|
})
|
package/src/index.ts
CHANGED
|
@@ -2,5 +2,7 @@ export { Login } from "./login"
|
|
|
2
2
|
export { AuthCallback } from "./callback"
|
|
3
3
|
export { AuthConfirm } from "./confirm"
|
|
4
4
|
export { createBrowserClient, createServiceClient } from "./clients"
|
|
5
|
-
export { requireAdmin, inviteUser } from "./admin"
|
|
5
|
+
export { createAdminClient, requireAdmin, inviteUser } from "./admin"
|
|
6
|
+
export { AdminPanel } from "./admin-panel"
|
|
7
|
+
export { handleListUsers, handleInviteUser, handleUpdateUser, handleDeleteUser } from "./admin-routes"
|
|
6
8
|
export { createAuthMiddleware } from "./middleware"
|