11'use client' ;
22
3- import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
3+ import { Card , CardContent } from '@/components/ui/card' ;
44import { PaymentMethodStatusBadge } from '@/components/admin/PaymentMethodStatusBadge' ;
55import { UserStatusBadge } from '@/components/admin/UserStatusBadge' ;
66import { CopyTextButton } from '@/components/admin/CopyEmailButton' ;
@@ -10,44 +10,52 @@ import ResetAPIKeyButton from './ResetAPIKeyButton';
1010import ResetToMagicLinkLoginButton from './ResetToMagicLinkLoginButton' ;
1111import { Button } from '@/components/ui/button' ;
1212import Link from 'next/link' ;
13- import { Webhook } from 'lucide-react' ;
13+ import { SquareArrowOutUpRight , Webhook } from 'lucide-react' ;
14+ import { createHash } from 'crypto' ;
15+
16+ function getGravatarUrl ( email : string , size : number = 80 ) : string {
17+ const hash = createHash ( 'md5' ) . update ( email . toLowerCase ( ) . trim ( ) ) . digest ( 'hex' ) ;
18+ return `https://www.gravatar.com/avatar/${ hash } ?s=${ size } &d=mp` ;
19+ }
1420
1521type UserAdminAccountInfoProps = UserDetailProps ;
1622
1723export function UserAdminAccountInfo ( user : UserAdminAccountInfoProps ) {
24+ const gravatarUrl = getGravatarUrl ( user . google_user_email ) ;
25+ const stripeUrl = `https://dashboard.stripe.com/${ process . env . NODE_ENV === 'development' ? 'test/' : '' } customers/${ user . stripe_customer_id } ` ;
26+ const hibpUrl = `https://haveibeenpwned.com/account/${ encodeURIComponent ( user . google_user_email ) } ` ;
27+
1828 return (
1929 < Card
20- className = { `flex-1 ${ user . blocked_reason || user . is_blacklisted_by_domain ? 'border-red-500 bg-red-950/50' : '' } ` }
30+ className = {
31+ user . blocked_reason || user . is_blacklisted_by_domain ? 'border-red-500 bg-red-950/50' : ''
32+ }
2133 >
22- < CardHeader >
23- < div className = "flex flex-wrap items-center gap-4" >
24- < div className = "flex min-w-max items-center gap-4" >
34+ < CardContent className = "pt-5" >
35+ { /* Top row: identity + badges/actions */ }
36+ < div className = "flex flex-wrap items-start justify-between gap-4" >
37+ < div className = "flex items-center gap-3" >
2538 < img
2639 src = { user . google_user_image_url }
2740 alt = { user . google_user_name }
28- className = "h-16 w-16 rounded-full"
41+ className = "h-12 w-12 rounded-full"
2942 onError = { e => {
3043 const target = e . target as HTMLImageElement ;
3144 target . src = '/default-avatar.svg' ;
3245 } }
3346 />
3447 < div >
35- < CardTitle className = "text-2xl " > { user . google_user_name } </ CardTitle >
36- < div className = "flex items-center gap-2 " >
37- < CardDescription className = "text-lg " > { user . google_user_email } </ CardDescription >
48+ < h2 className = "text-lg font-semibold leading-tight " > { user . google_user_name } </ h2 >
49+ < div className = "flex items-center gap-1 " >
50+ < span className = "text-muted-foreground text-sm " > { user . google_user_email } </ span >
3851 < CopyTextButton text = { user . google_user_email } />
3952 </ div >
4053 </ div >
4154 </ div >
42- < div className = "ml-auto flex min-w-min shrink flex-wrap items-center gap-2" >
55+
56+ < div className = "flex flex-wrap items-center gap-2" >
4357 < UserStatusBadge is_detail = { true } user = { user } />
4458 < PaymentMethodStatusBadge paymentMethodStatus = { user . paymentMethodStatus } />
45- </ div >
46- </ div >
47- </ CardHeader >
48- < CardContent >
49- < div className = "flex flex-row-reverse flex-wrap justify-between gap-6" >
50- < div className = "flex grow basis-auto flex-col items-end space-y-2" >
5159 < ResetAPIKeyButton userId = { user . id } />
5260 { ! user . is_sso_protected_domain && < ResetToMagicLinkLoginButton userId = { user . id } /> }
5361 < Button variant = "outline" size = "sm" asChild >
@@ -57,75 +65,95 @@ export function UserAdminAccountInfo(user: UserAdminAccountInfoProps) {
5765 </ Button >
5866 < Button variant = "outline" size = "sm" asChild >
5967 < Link href = { `/admin/users/${ encodeURIComponent ( user . id ) } /webhooks` } >
60- < Webhook className = "mr-2 h-4 w-4 " />
61- View webhooks
68+ < Webhook className = "mr-1 h-3.5 w-3.5 " />
69+ Webhooks
6270 </ Link >
6371 </ Button >
72+ < a
73+ href = { stripeUrl }
74+ target = "_blank"
75+ rel = "noopener noreferrer"
76+ className = "inline-flex items-center gap-1.5 rounded-md bg-purple-950 px-2.5 py-1.5 text-xs font-medium text-purple-200 transition-colors hover:bg-purple-900"
77+ >
78+ Stripe
79+ < SquareArrowOutUpRight size = { 12 } />
80+ </ a >
81+ < a
82+ href = { hibpUrl }
83+ target = "_blank"
84+ rel = "noopener noreferrer"
85+ className = "inline-flex items-center gap-1.5 rounded-md bg-orange-950 px-2.5 py-1.5 text-xs font-medium text-orange-200 transition-colors hover:bg-orange-900"
86+ title = "Check if this email has been exposed in any data breaches"
87+ >
88+ HIBP
89+ < SquareArrowOutUpRight size = { 12 } />
90+ </ a >
91+ < img
92+ src = { gravatarUrl }
93+ alt = { `Gravatar for ${ user . google_user_name } ` }
94+ className = "border-border h-7 w-7 rounded-full border"
95+ title = "Gravatar"
96+ />
6497 </ div >
65- < div className = "grow basis-auto space-y-4" >
66- < div >
67- < h4 className = "text-muted-foreground text-sm font-medium" > Updated At</ h4 >
68- < p > { formatDate ( user . updated_at ) } </ p >
69- </ div >
70- < div >
71- < h4 className = "text-muted-foreground text-sm font-medium" > Hosted Domain</ h4 >
72- < p >
73- { user . hosted_domain || 'N/A' } { ' ' }
74- { user . hosted_domain && < CopyTextButton text = { user . hosted_domain } /> }
75- </ p >
76- </ div >
77- </ div >
78- < div className = "grow basis-auto space-y-4" >
79- < div >
80- < h4 className = "text-muted-foreground text-sm font-medium" > User ID</ h4 >
81- < p className = "font-mono text-sm break-all" >
82- { user . id } < CopyTextButton text = { user . id } />
83- </ p >
84- </ div >
85- < div >
86- < h4 className = "text-muted-foreground text-sm font-medium" > Email</ h4 >
87- < div className = "flex items-center gap-2" >
88- < p className = "break-all" > { user . google_user_email } </ p >
89- < CopyTextButton text = { user . google_user_email } />
90- </ div >
91- </ div >
92- < div >
93- < h4 className = "text-muted-foreground text-sm font-medium" > Created At</ h4 >
94- < p > { formatDate ( user . created_at ) } </ p >
95- </ div >
96- < div >
97- < h4 className = "text-muted-foreground text-sm font-medium" >
98- OpenRouter Upstream Safety Identifier
99- </ h4 >
100- { user . openrouter_upstream_safety_identifier ? (
101- < div className = "flex items-center gap-2" >
102- < p className = "font-mono text-sm break-all" >
103- { user . openrouter_upstream_safety_identifier }
104- </ p >
105- < CopyTextButton text = { user . openrouter_upstream_safety_identifier } />
106- </ div >
107- ) : (
108- < p className = "text-muted-foreground" > N/A</ p >
109- ) }
110- </ div >
111- < div >
112- < h4 className = "text-muted-foreground text-sm font-medium" >
113- Vercel Downstream Safety Identifier
114- </ h4 >
115- { user . vercel_downstream_safety_identifier ? (
116- < div className = "flex items-center gap-2" >
117- < p className = "font-mono text-sm break-all" >
118- { user . vercel_downstream_safety_identifier }
119- </ p >
120- < CopyTextButton text = { user . vercel_downstream_safety_identifier } />
121- </ div >
122- ) : (
123- < p className = "text-muted-foreground" > N/A</ p >
124- ) }
125- </ div >
126- </ div >
98+ </ div >
99+
100+ { /* Metadata grid */ }
101+ < div className = "mt-4 grid grid-cols-2 gap-x-8 gap-y-3 sm:grid-cols-3 lg:grid-cols-4" >
102+ < Field label = "User ID" mono >
103+ { user . id } < CopyTextButton text = { user . id } />
104+ </ Field >
105+ < Field label = "Email" >
106+ { user . google_user_email } < CopyTextButton text = { user . google_user_email } />
107+ </ Field >
108+ < Field label = "Hosted Domain" >
109+ { user . hosted_domain || 'N/A' }
110+ { user . hosted_domain ? < CopyTextButton text = { user . hosted_domain } /> : null }
111+ </ Field >
112+ < Field label = "Created" > { formatDate ( user . created_at ) } </ Field >
113+ < Field label = "Updated" > { formatDate ( user . updated_at ) } </ Field >
114+ < Field label = "OpenRouter Upstream Safety ID" mono >
115+ { user . openrouter_upstream_safety_identifier ? (
116+ < >
117+ { user . openrouter_upstream_safety_identifier }
118+ < CopyTextButton text = { user . openrouter_upstream_safety_identifier } />
119+ </ >
120+ ) : (
121+ < span className = "text-muted-foreground" > N/A</ span >
122+ ) }
123+ </ Field >
124+ < Field label = "Vercel Downstream Safety ID" mono >
125+ { user . vercel_downstream_safety_identifier ? (
126+ < >
127+ { user . vercel_downstream_safety_identifier }
128+ < CopyTextButton text = { user . vercel_downstream_safety_identifier } />
129+ </ >
130+ ) : (
131+ < span className = "text-muted-foreground" > N/A</ span >
132+ ) }
133+ </ Field >
127134 </ div >
128135 </ CardContent >
129136 </ Card >
130137 ) ;
131138}
139+
140+ function Field ( {
141+ label,
142+ mono,
143+ children,
144+ } : {
145+ label : string ;
146+ mono ?: boolean ;
147+ children : React . ReactNode ;
148+ } ) {
149+ return (
150+ < div className = "min-w-0" >
151+ < h4 className = "text-muted-foreground text-xs font-medium" > { label } </ h4 >
152+ < div
153+ className = { `flex items-center gap-1 text-sm break-all ${ mono ? 'font-mono text-xs' : '' } ` }
154+ >
155+ { children }
156+ </ div >
157+ </ div >
158+ ) ;
159+ }
0 commit comments