mirror of
https://github.com/supabase/supabase.git
synced 2026-05-10 17:11:21 +08:00
## Problem We want to reduce the code we ship and maintain. ## Solution - Migrate old `Input` usage to the new Shadcn `input` This PR focuses on: - DevToolbar - design-system examples - `www` usages - `docs` usages ## Screenshots ### Docs: OAuth Apple Before: <img width="613" height="508" alt="image" src="https://github.com/user-attachments/assets/1d2d7726-cc5e-471f-a2c2-995b9d7f70ee" /> After: <img width="606" height="530" alt="image" src="https://github.com/user-attachments/assets/ca4f522f-de9c-4edf-966b-70cad5015d0c" /> NOTE: Also used the `DataInput` for the secret once the inputs are filled. ### Docs: Extensions Before: <img width="596" height="161" alt="image" src="https://github.com/user-attachments/assets/16d2f548-90dc-4987-9954-7c47ac58e76e" /> After: <img width="604" height="227" alt="image" src="https://github.com/user-attachments/assets/62c74102-98c6-47a6-b19b-cbf67dfad68f" /> ### WWW: Blog search Before: <img width="971" height="417" alt="image" src="https://github.com/user-attachments/assets/efb0307e-60b5-4d8f-9823-c8b8996cdf32" /> After: <img width="964" height="403" alt="image" src="https://github.com/user-attachments/assets/2dc0decd-b773-4bc6-9a72-c43f352f8cbf" /> ### WWW: Blog author search Before: <img width="953" height="337" alt="image" src="https://github.com/user-attachments/assets/1f629704-ab7d-4e4b-878e-1838ab16037f" /> After: <img width="1028" height="341" alt="image" src="https://github.com/user-attachments/assets/d8d54dcb-3c00-46ea-b97f-55c16cda917f" /> ### WWW: Assistant demo Before: <img width="421" height="715" alt="image" src="https://github.com/user-attachments/assets/bcc4a591-d53c-4202-acf8-2b3d6cfd52d2" /> After: <img width="435" height="731" alt="image" src="https://github.com/user-attachments/assets/8a57c5da-5c9e-474d-a89e-2835d3498aef" /> ### WWW: Integrations Before: <img width="740" height="599" alt="image" src="https://github.com/user-attachments/assets/cf3d3d8a-b247-4e20-b47d-11976ca49c57" /> After: <img width="911" height="492" alt="image" src="https://github.com/user-attachments/assets/dcb5b6e8-f4e2-4801-b390-352390a0b486" /> ### WWW: features Before: <img width="1098" height="491" alt="image" src="https://github.com/user-attachments/assets/ea3645c5-df03-4eb9-b28c-41018e01c41e" /> After: <img width="976" height="479" alt="image" src="https://github.com/user-attachments/assets/4439a38e-6342-42cd-a859-1e599a8cf0f4" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Adjusted input widths and spacing for more consistent search and form layouts. * **Refactor** * Standardized input components across apps and docs, and reimplemented search controls as composed input groups with dedicated icon/action areas. * **Chores** * Removed a deprecated legacy input variant along with its legacy styles and tests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
211 lines
5.7 KiB
TypeScript
211 lines
5.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { Button, Input_Shadcn_ as Input } from 'ui'
|
|
import { Admonition } from 'ui-patterns/admonition'
|
|
import { Input as DataInput } from 'ui-patterns/DataInputs/Input'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
|
|
function base64URL(value: string) {
|
|
return globalThis.btoa(value).replace(/[=]/g, '').replace(/[+]/g, '-').replace(/[\/]/g, '_')
|
|
}
|
|
|
|
/*
|
|
Convert a string into an ArrayBuffer
|
|
from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
|
*/
|
|
function stringToArrayBuffer(value: string) {
|
|
const buf = new ArrayBuffer(value.length)
|
|
const bufView = new Uint8Array(buf)
|
|
for (let i = 0; i < value.length; i++) {
|
|
bufView[i] = value.charCodeAt(i)
|
|
}
|
|
return buf
|
|
}
|
|
|
|
function arrayBufferToString(buf) {
|
|
return String.fromCharCode.apply(null, new Uint8Array(buf))
|
|
}
|
|
|
|
const generateAppleSecretKey = async (
|
|
kid: string,
|
|
iss: string,
|
|
sub: string,
|
|
file: File
|
|
): Promise<{ kid: string; jwt: string; exp: number }> => {
|
|
if (!kid) {
|
|
const match = file.name.match(/AuthKey_([^.]+)[.].*$/i)
|
|
if (match && match[1]) {
|
|
kid = match[1]
|
|
}
|
|
}
|
|
|
|
if (!kid) {
|
|
throw new Error(
|
|
`No Key ID provided. The file "${file.name}" does not follow the AuthKey_XXXXXXXXXX.p8 pattern. Please provide a Key ID manually.`
|
|
)
|
|
}
|
|
|
|
const contents = await file.text()
|
|
|
|
if (!contents.match(/^\s*-+BEGIN PRIVATE KEY-+[^-]+-+END PRIVATE KEY-+\s*$/i)) {
|
|
throw new Error(`Chosen file does not appear to be a PEM encoded PKCS8 private key file.`)
|
|
}
|
|
|
|
// remove PEM headers and spaces
|
|
const pkcs8 = stringToArrayBuffer(
|
|
globalThis.atob(contents.replace(/-+[^-]+-+/g, '').replace(/\s+/g, ''))
|
|
)
|
|
|
|
const privateKey = await globalThis.crypto.subtle.importKey(
|
|
'pkcs8',
|
|
pkcs8,
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign']
|
|
)
|
|
|
|
const iat = Math.floor(Date.now() / 1000)
|
|
const exp = iat + 180 * 24 * 60 * 60
|
|
|
|
const jwt = [
|
|
base64URL(JSON.stringify({ typ: 'JWT', kid, alg: 'ES256' })),
|
|
base64URL(
|
|
JSON.stringify({
|
|
iss,
|
|
sub,
|
|
iat,
|
|
exp,
|
|
aud: 'https://appleid.apple.com',
|
|
})
|
|
),
|
|
]
|
|
|
|
const signature = await globalThis.crypto.subtle.sign(
|
|
{
|
|
name: 'ECDSA',
|
|
hash: 'SHA-256',
|
|
},
|
|
privateKey,
|
|
stringToArrayBuffer(jwt.join('.'))
|
|
)
|
|
|
|
jwt.push(base64URL(arrayBufferToString(signature)))
|
|
|
|
return { kid, jwt: jwt.join('.'), exp }
|
|
}
|
|
|
|
const AppleSecretGenerator = () => {
|
|
const [file, setFile] = useState({ file: null as File | null })
|
|
const [teamID, setTeamID] = useState('')
|
|
const [serviceID, setServiceID] = useState('')
|
|
const [keyID, setKeyID] = useState('')
|
|
const [secretKey, setSecretKey] = useState('')
|
|
const [expiresAt, setExpiresAt] = useState('')
|
|
const [error, setError] = useState('')
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
layout="vertical"
|
|
label="Account ID"
|
|
description="Found in the upper-right corner of Apple Developer Center."
|
|
labelOptional="required"
|
|
>
|
|
<Input
|
|
placeholder="Apple Developer account ID, 10 alphanumeric digits"
|
|
value={teamID}
|
|
onChange={(e) => setTeamID(e.target.value.trim())}
|
|
/>
|
|
</FormItemLayout>
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
layout="vertical"
|
|
label="Service ID"
|
|
description="Found under Certificates, Identifiers & Profiles in Apple Developer Center."
|
|
labelOptional="required"
|
|
>
|
|
<Input
|
|
placeholder="ID of the service, example: com.example.app.service"
|
|
value={serviceID}
|
|
onChange={(e) => setServiceID(e.target.value.trim())}
|
|
/>
|
|
</FormItemLayout>
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
layout="vertical"
|
|
label="Key ID"
|
|
description="If the file you select does not preserve the original name from Apple Developer Center, please enter the key ID."
|
|
labelOptional="(optional)"
|
|
>
|
|
<Input
|
|
placeholder="Extracted from filename, AuthKey_XXXXXXXXXX.p8"
|
|
value={keyID}
|
|
onChange={(e) => setKeyID(e.target.value.trim())}
|
|
/>
|
|
</FormItemLayout>
|
|
<div>
|
|
<input
|
|
type="file"
|
|
onChange={(e) => {
|
|
setFile({ file: e.target.files?.[0] || null })
|
|
}}
|
|
/>
|
|
</div>
|
|
<div style={{ height: '1rem' }} />
|
|
|
|
<Button
|
|
size="medium"
|
|
disabled={
|
|
!(
|
|
teamID.length === 10 &&
|
|
serviceID &&
|
|
((globalThis && globalThis.showOpenFilePicker) || file.file)
|
|
)
|
|
}
|
|
onClick={async () => {
|
|
setError('')
|
|
|
|
try {
|
|
const { kid, jwt, exp } = await generateAppleSecretKey(
|
|
keyID,
|
|
teamID,
|
|
serviceID,
|
|
file.file!
|
|
)
|
|
setKeyID(kid)
|
|
setSecretKey(jwt)
|
|
setExpiresAt(new Date(exp * 1000).toString())
|
|
setError('')
|
|
} catch (e: any) {
|
|
setError(e.message)
|
|
console.error(e)
|
|
}
|
|
}}
|
|
>
|
|
Generate Secret Key
|
|
</Button>
|
|
|
|
{error && <Admonition type="danger">{error}</Admonition>}
|
|
|
|
{secretKey && (
|
|
<>
|
|
<div style={{ height: '1rem' }} />
|
|
<FormItemLayout
|
|
isReactForm={false}
|
|
layout="vertical"
|
|
label="Secret Key"
|
|
description={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`}
|
|
>
|
|
<DataInput value={secretKey} copy reveal />
|
|
</FormItemLayout>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AppleSecretGenerator
|