Help Center/ Relational Database Service_RDS for PostgreSQL/ Best Practices/ AI Application Development Platform/ Source Code Configuration Guide for RDS Supabase GitHub SSO Demo
Updated on 2026-10-30 GMT+08:00
Copied.
Source Code Configuration Guide for RDS Supabase GitHub SSO Demo
Scenarios
This section describes how to create a demo project and all source code files. Create a project and related directories on the local machine strictly according to the directory and file structure. Create the corresponding files in sequence and copy all source code.
Directory and File Structure
Figure 1 Directory and file structure of the demo project
The directory and file structure of the demo project is as follows:
supabase_auth_github // Project root directory ├── .env // Supabase environment variable configuration ├── index.html // Homepage ├── package.json // Build dependency configuration ├── vite.config.js └── src // Core functional source code directory ├── App.jsx ├── index.css ├── main.jsx ├── components │ ├── Dashboard.jsx │ └── Login.jsx ├── context │ ├── AuthContext.jsx │ └── LanguageContext.jsx ├── i18n │ └── index.js └── lib └── supabaseClient.js
File Content
.env
# Replace with the public access address of RDS Supabase.
VITE_SUPABASE_URL={supabase_url}
# Replace with the anon API key of RDS Supabase.
VITE_SUPABASE_ANON_KEY={supabase_api_key} index.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Supabase GitHub SSO Sign-in</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html> package.json
{
"name": "supabase-github-sso",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@supabase/supabase-js": "^2.45.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2"
},
"devDependencies": {
"@vitejs/plugin-react": "^5.0.0",
"vite": "^6.3.0"
}
} vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: true,
port: 5173
}
}) App.jsx
import { Navigate, Route, Routes } from 'react-router-dom'
import { AuthProvider, useAuth } from './context/AuthContext'
import { LanguageProvider, useLang } from './context/LanguageContext'
import Login from './components/Login'
import Dashboard from './components/Dashboard'
function LangSwitch() {
const { lang, toggleLang } = useLang()
return (
<button className="lang-switch" onClick={toggleLang} aria-label="Switch language">
{lang === 'zh' ? 'EN' : 'zh'}
</button>
)
}
function ProtectedRoute({ children }) {
const { session, loading } = useAuth()
const { t } = useLang()
if (loading) {
return (
<div className="full-center">
<span className="spinner spinner-lg" />
<p>{t.loadingSession}</p>
</div>
)
}
if (!session) {
return <Navigate to="/" replace />
}
return children
}
function RootRoute() {
const { session, loading } = useAuth()
const { t } = useLang()
if (loading) {
return (
<div className="full-center">
<span className="spinner spinner-lg" />
<p>{t.loadingSession}</p>
</div>
)
}
if (session) {
return <Navigate to="/demo" replace />
}
return <Login />
}
export default function App() {
return (
<LanguageProvider>
<AuthProvider>
<LangSwitch />
<Routes>
<Route path="/" element={<RootRoute />} />
<Route
path="/demo"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AuthProvider>
</LanguageProvider>
)
} index.css
:root {
--color-primary: #3ecf8e;
--color-primary-dark: #1a7f5a;
--color-bg: #f0fdf4;
--color-bg-gradient-start: #f0fdf4;
--color-bg-gradient-end: #dcfce7;
--color-card: #ffffff;
--color-text: #1f2937;
--color-text-secondary: #6b7280;
--color-border: #bbf7d0;
--color-error: #ef4444;
--color-error-bg: #fef2f2;
--color-success: #10b981;
--color-success-bg: #ecfdf5;
--color-info: #3b82f6;
--color-info-bg: #eff6ff;
--color-elevated: #f0fdf4;
--radius: 12px;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 16px rgba(0, 0, 0, 0.04);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body,
#root {
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter',
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Roboto, sans-serif;
background: linear-gradient(135deg, var(--color-bg-gradient-start) 0%, var(--color-bg-gradient-end) 100%);
color: var(--color-text);
-webkit-font-smoothing: antialiased;
}
/* ===== Language switch button ===== */
.lang-switch {
position: fixed;
top: 1rem;
right: 1rem;
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--color-border);
background: var(--color-card);
color: var(--color-text);
font-size: 13px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
z-index: 100;
transition: all 0.2s ease;
}
.lang-switch:hover {
border-color: var(--color-primary);
color: var(--color-primary);
transform: scale(1.05);
}
/* ===== Decorative background glow ===== */
.auth-page,
.dashboard-page {
position: relative;
}
.auth-page::before,
.auth-page::after,
.dashboard-page::before,
.dashboard-page::after {
content: '';
position: fixed;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
pointer-events: none;
z-index: 0;
}
.auth-page::before,
.dashboard-page::before {
width: 400px;
height: 400px;
background: var(--color-primary);
top: -100px;
right: -100px;
}
.auth-page::after,
.dashboard-page::after {
width: 300px;
height: 300px;
background: var(--color-primary-dark);
bottom: -50px;
left: -50px;
opacity: 0.2;
}
.auth-page > *,
.dashboard-page > * {
position: relative;
z-index: 1;
}
/* ===== Full-page centered loading ===== */
.full-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
height: 100vh;
color: var(--color-text-secondary);
}
/* ===== Login page ===== */
.auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
}
.auth-card {
width: 100%;
max-width: 520px;
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 40px 32px;
text-align: center;
box-shadow: var(--shadow);
}
.auth-logo {
width: 84px;
height: 84px;
margin: 0 auto 20px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.auth-title {
margin-bottom: 24px;
white-space: nowrap;
}
.auth-title-highlight {
display: block;
font-size: 32px;
font-weight: 800;
letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
position: relative;
padding-bottom: 16px;
}
.auth-title-highlight::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 3px;
border-radius: 2px;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
}
.auth-subtitle {
color: var(--color-text-secondary);
font-size: 13px;
margin-bottom: 28px;
letter-spacing: 0.02em;
}
.btn-github {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 12px 20px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: var(--color-primary);
border: 1px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease, transform 0.1s ease;
}
.btn-github:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-github:active:not(:disabled) {
transform: scale(0.98);
}
.btn-github:disabled {
opacity: 0.6;
cursor: default;
}
.auth-error {
margin-top: 16px;
padding: 10px 12px;
font-size: 13px;
color: #991b1b;
background: var(--color-error-bg);
border: 1px solid #fecaca;
border-radius: 8px;
}
.auth-flow {
margin-top: 24px;
padding: 14px 16px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 10px;
text-align: left;
}
.auth-flow-title {
font-size: 12px;
font-weight: 600;
color: var(--color-primary-dark);
letter-spacing: 0.05em;
margin-bottom: 12px;
text-transform: uppercase;
}
.auth-flow-steps {
list-style: none;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.auth-flow-steps li {
display: flex;
gap: 8px;
align-items: flex-start;
}
.auth-flow-step-num {
flex-shrink: 0;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--color-primary);
color: #fff;
font-size: 11px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1px;
}
.auth-flow-step-content {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.auth-flow-step-content strong {
font-size: 12px;
font-weight: 600;
color: var(--color-text);
}
.auth-flow-step-content span {
font-size: 11px;
color: var(--color-text-secondary);
line-height: 1.4;
}
/* ===== Dashboard page ===== */
.dashboard-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
}
.dashboard-card {
width: 100%;
max-width: 520px;
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
}
.dashboard-header {
display: flex;
flex-direction: column;
gap: 8px;
padding: 24px 24px 20px;
border-bottom: 1px solid var(--color-border);
background: linear-gradient(135deg, var(--color-bg-gradient-start), var(--color-bg-gradient-end));
}
.dashboard-header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.github-corner {
flex-shrink: 0;
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid var(--color-border);
background: var(--color-card);
color: var(--color-text);
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
transition: all 0.2s ease;
}
.github-corner:hover {
border-color: var(--color-primary);
color: var(--color-primary);
transform: scale(1.05);
}
.dashboard-theme {
white-space: nowrap;
}
.dashboard-theme-highlight {
display: inline-block;
font-size: 28px;
font-weight: 800;
letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
position: relative;
padding-bottom: 14px;
}
.dashboard-theme-highlight::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 40px;
height: 3px;
border-radius: 2px;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
}
.dashboard-welcome {
font-size: 15px;
font-weight: 400;
color: var(--color-text-secondary);
letter-spacing: 0.02em;
}
.dashboard-welcome-name {
color: var(--color-primary-dark);
font-weight: 600;
}
.btn-logout:disabled {
opacity: 0.6;
cursor: default;
}
.profile {
display: flex;
align-items: center;
gap: 18px;
padding: 28px 24px;
}
.avatar {
width: 72px;
height: 72px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--color-border);
flex-shrink: 0;
}
.avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: #fff;
font-size: 28px;
font-weight: 700;
}
.profile-info {
min-width: 0;
}
.profile-name {
font-size: 20px;
font-weight: 700;
color: var(--color-text);
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-username {
color: var(--color-primary-dark);
font-size: 14px;
margin-bottom: 2px;
}
.profile-email {
color: var(--color-text-secondary);
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-list {
padding: 0 24px 24px;
display: flex;
flex-direction: column;
}
.detail-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 0;
cursor: pointer;
transition: background 0.2s ease;
border-bottom: 1px solid var(--color-border);
}
.detail-row:first-child {
padding-top: 0;
}
.detail-row:last-child {
border-bottom: none;
}
.detail-row:hover {
background: var(--color-bg);
margin: 0 -12px;
padding-left: 12px;
padding-right: 12px;
border-radius: 6px;
}
.detail-label {
color: var(--color-text-secondary);
font-size: 13px;
flex-shrink: 0;
font-weight: 500;
}
.detail-value {
font-size: 13px;
color: var(--color-text);
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 320px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
}
.detail-token {
color: var(--color-primary-dark);
}
.detail-row-token {
cursor: default;
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.detail-row-token:hover {
background: transparent;
margin: 0;
padding-left: 0;
padding-right: 0;
border-radius: 0;
}
.token-field {
display: flex;
gap: 8px;
}
.token-input {
flex: 1;
padding: 8px 12px;
font-size: 13px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
color: var(--color-text);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 8px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.token-input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(62, 207, 142, 0.15);
}
.token-copy-btn {
flex-shrink: 0;
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
color: #fff;
background: var(--color-primary);
border: 1px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease, transform 0.1s ease;
}
.token-copy-btn:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.token-copy-btn:active:not(:disabled) {
transform: scale(0.96);
}
.token-copy-btn:disabled {
opacity: 0.6;
cursor: default;
}
.detail-row-logout {
cursor: default;
padding-top: 20px;
}
.detail-row-logout:hover {
background: transparent;
margin: 0;
padding-left: 0;
padding-right: 0;
border-radius: 0;
}
.btn-logout {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 12px 20px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: var(--color-primary);
border: 1px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease, transform 0.1s ease;
}
.btn-logout:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.btn-logout:active:not(:disabled) {
transform: scale(0.98);
}
/* ===== Loading spinner ===== */
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.spinner-lg {
width: 36px;
height: 36px;
border-width: 3px;
border-color: var(--color-border);
border-top-color: var(--color-primary);
}
@keyframes spin {
to {
transform: rotate(360deg);
}
} main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
) Dashboard.jsx
import { useAuth } from '../context/AuthContext'
import { useLang } from '../context/LanguageContext'
import { useNavigate } from 'react-router-dom'
import { useState, useMemo } from 'react'
export default function Dashboard() {
const { user, session, signOut } = useAuth()
const { t, lang } = useLang()
const navigate = useNavigate()
const [signingOut, setSigningOut] = useState(false)
const [tokenCopied, setTokenCopied] = useState(false)
const meta = user?.user_metadata ?? {}
const name = meta.full_name || meta.name || meta.user_name || t.githubUser
const username = meta.user_name
const avatar = meta.avatar_url
const email = user?.email || meta.email
const loginTime = useMemo(() => {
const raw = session?.created_at || user?.created_at
if (!raw) return '—'
const d = new Date(raw)
if (isNaN(d.getTime())) return '—'
return d.toLocaleString(lang === 'zh' ? 'zh-CN' : 'en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}, [session?.created_at, user?.created_at, lang])
const handleSignOut = async () => {
setSigningOut(true)
const ok = await signOut()
setSigningOut(false)
if (ok) navigate('/', { replace: true })
}
const copyToClipboard = async (text) => {
if (!text) return false
try {
await navigator.clipboard.writeText(text)
return true
} catch {
try {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
const ok = document.execCommand('copy')
document.body.removeChild(textarea)
return ok
} catch {
return false
}
}
}
const handleCopyToken = async () => {
const token = session?.access_token
if (!token) return
const ok = await copyToClipboard(token)
if (ok) {
setTokenCopied(true)
setTimeout(() => setTokenCopied(false), 2000)
}
}
const handleCopy = async (text) => {
await copyToClipboard(text)
}
return (
<div className="dashboard-page">
<div className="dashboard-card">
<header className="dashboard-header">
<div className="dashboard-header-top">
<h2 className="dashboard-theme">
<span className="dashboard-theme-highlight">Supabase GitHub SSO</span>
</h2>
<a
href="https://github.com"
target="_blank"
rel="noopener noreferrer"
className="github-corner"
aria-label="GitHub"
>
<svg viewBox="0 0 16 16" width="20" height="20" aria-hidden="true">
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
</a>
</div>
<p className="dashboard-welcome">
{t.dashboardWelcome}
</p>
</header>
<div className="profile">
{avatar ? (
<img className="avatar" src={avatar} alt={name} />
) : (
<div className="avatar avatar-fallback">{name.charAt(0).toUpperCase()}</div>
)}
<div className="profile-info">
<h3 className="profile-name">{name}</h3>
{username && <p className="profile-username">@{username}</p>}
{email && <p className="profile-email">{email}</p>}
</div>
</div>
<div className="detail-list">
<div className="detail-row" onClick={() => handleCopy(user?.id)}>
<span className="detail-label">{t.userId}</span>
<span className="detail-value" title={user?.id}>{user?.id ?? '—'}</span>
</div>
<div className="detail-row" onClick={() => handleCopy(email)}>
<span className="detail-label">{t.email}</span>
<span className="detail-value" title={email}>{email ?? '—'}</span>
</div>
<div className="detail-row detail-row-token">
<span className="detail-label">{t.accessToken}</span>
<div className="token-field">
<input
type="password"
className="token-input"
value={session?.access_token ?? ''}
readOnly
/>
<button
type="button"
className="token-copy-btn"
onClick={handleCopyToken}
disabled={!session?.access_token}
>
{tokenCopied ? t.copied : t.copyToken}
</button>
</div>
</div>
<div className="detail-row">
<span className="detail-label">{t.loginMethod}</span>
<span className="detail-value">
{user?.app_metadata?.provider ?? 'github'}
</span>
</div>
<div className="detail-row">
<span className="detail-label">{t.loginTime}</span>
<span className="detail-value">{loginTime}</span>
</div>
<div className="detail-row detail-row-logout">
<button
type="button"
className="btn-logout"
onClick={handleSignOut}
disabled={signingOut}
>
{signingOut ? t.loggingOut : t.logout}
</button>
</div>
</div>
</div>
</div>
)
} Login.jsx
import { useState } from 'react'
import { useAuth } from '../context/AuthContext'
import { useLang } from '../context/LanguageContext'
export default function Login() {
const { signInWithGitHub, error } = useAuth()
const { t } = useLang()
const [pending, setPending] = useState(false)
const handleGitHubLogin = async () => {
setPending(true)
await signInWithGitHub()
setPending(false)
}
return (
<div className="auth-page">
<div className="auth-card">
<div className="auth-logo">
<svg viewBox="0 0 24 24" width="56" height="56" aria-hidden="true">
<path
fill="#1f2937"
d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.88-.01-1.73-2.78.62-3.37-1.37-3.37-1.37-.46-1.18-1.11-1.5-1.11-1.5-.91-.64.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.37-2.22-.26-4.55-1.14-4.55-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.28 2.75 1.05A9.36 9.36 0 0 1 12 6.84c.85 0 1.71.12 2.51.35 1.91-1.33 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.81-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.48-.01 2.82 0 .27.18.6.69.49A10.26 10.26 0 0 0 22 12.25C22 6.58 17.52 2 12 2z"
/>
</svg>
</div>
<h1 className="auth-title">
<span className="auth-title-highlight">Supabase GitHub SSO</span>
</h1>
<p className="auth-subtitle">{t.loginSubtitle}</p>
<button
type="button"
className="btn-github"
onClick={handleGitHubLogin}
disabled={pending}
>
{pending ? (
<>
<span className="spinner" aria-hidden="true" />
{t.loginPending}
</>
) : (
<>
<svg viewBox="0 0 16 16" width="20" height="20" aria-hidden="true">
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
</svg>
{t.loginButton}
</>
)}
</button>
{error && <p className="auth-error" role="alert">{error}</p>}
<div className="auth-flow">
<p className="auth-flow-title">{t.flowTitle}</p>
<ol className="auth-flow-steps">
<li>
<span className="auth-flow-step-num">1</span>
<div className="auth-flow-step-content">
<strong>{t.flowStep1Title}</strong>
<span>{t.flowStep1Desc}</span>
</div>
</li>
<li>
<span className="auth-flow-step-num">2</span>
<div className="auth-flow-step-content">
<strong>{t.flowStep2Title}</strong>
<span>{t.flowStep2Desc}</span>
</div>
</li>
<li>
<span className="auth-flow-step-num">3</span>
<div className="auth-flow-step-content">
<strong>{t.flowStep3Title}</strong>
<span>{t.flowStep3Desc}</span>
</div>
</li>
<li>
<span className="auth-flow-step-num">4</span>
<div className="auth-flow-step-content">
<strong>{t.flowStep4Title}</strong>
<span>{t.flowStep4Desc}</span>
</div>
</li>
</ol>
</div>
</div>
</div>
)
} AuthContext.jsx
import { createContext, useContext, useEffect, useState, useCallback } from 'react'
import { supabase } from '../lib/supabaseClient'
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const [session, setSession] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let mounted = true
// Obtain the current session during initialization.
supabase.auth.getSession().then(({ data, error }) => {
if (!mounted) return
if (error) {
setError(error.message)
} else {
setSession(data.session)
}
setLoading(false)
})
// Listen for authentication status changes.
const { data: listener } = supabase.auth.onAuthStateChange((_event, newSession) => {
setSession(newSession)
setError(null)
})
return () => {
mounted = false
listener.subscription.unsubscribe()
}
}, [])
// Sign in with GitHub OAuth.
const signInWithGitHub = useCallback(async () => {
setError(null)
const { error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: window.location.origin
}
})
if (error) {
setError(error.message)
return false
}
return true
}, [])
// Sign-out
const signOut = useCallback(async () => {
setError(null)
const { error } = await supabase.auth.signOut()
if (error) {
setError(error.message)
return false
}
setSession(null)
return true
}, [])
const value = {
session,
user: session?.user ?? null,
loading,
error,
signInWithGitHub,
signOut
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (ctx === null) {
throw new Error('useAuth must be used inside AuthProvider.');
}
return ctx
} LanguageContext.jsx
import { createContext, useContext, useState, useCallback, useEffect } from 'react'
import { translations } from '../i18n'
const LanguageContext = createContext(null)
export function LanguageProvider({ children }) {
const [lang, setLang] = useState(() => {
const saved = localStorage.getItem('app-lang')
return saved === 'en' ? 'en' : 'zh'
})
useEffect(() => {
localStorage.setItem('app-lang', lang)
}, [lang])
const toggleLang = useCallback(() => {
setLang((prev) => (prev === 'zh' ? 'en' : 'zh'))
}, [])
const t = translations[lang]
const value = { lang, toggleLang, t }
return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>
}
export function useLang() {
const ctx = useContext(LanguageContext)
if (ctx === null) {
throw new Error('useLang must be used within LanguageProvider')
}
return ctx
} index.js
export const translations = {
en: {
// Login page
loginSubtitle: 'Sign in with your GitHub account to continue',
loginButton: 'Sign in with GitHub',
loginPending: 'Redirecting to GitHub...',
flowTitle: 'SSO Authentication Flow',
flowStep1Title: 'Initiate Auth',
flowStep1Desc: 'After clicking, the app sends an OAuth request to GitHub via Supabase',
flowStep2Title: 'GitHub Authorize',
flowStep2Desc: 'Redirect to GitHub where you authorize the app to access your account',
flowStep3Title: 'Callback & Token',
flowStep3Desc: 'GitHub redirects back with a code, Supabase exchanges it for an Access Token',
flowStep4Title: 'Establish Session',
flowStep4Desc: 'Supabase creates a user session and returns to the app, completing login',
// Dashboard
dashboardWelcome: 'Login successful, welcome to RDS AI Application Development Platform',
userId: 'User ID',
email: 'Email',
accessToken: 'Access Token',
copyToken: 'Copy',
copied: 'Copied',
loginMethod: 'Login Method',
loginTime: 'Login Time',
logout: 'Sign Out',
loggingOut: 'Signing out...',
githubUser: 'GitHub User',
// App loading
loadingSession: 'Loading session...',
},
} supabaseClient.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
'Missing Supabase environment variables. Configure VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in the .env file.'
)
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: true,
autoRefreshToken: true,
detectSessionInUrl: true
}
}) Parent Topic: AI Application Development Platform
What is your overall rating for this page?
0
1
2
3
4
5
6
7
8
9
10
Very dissatisfiedVery satisfied
Thank you very much for your feedback. We will continue working to improve the documentation.See the reply and handling status in My Cloud VOC.
The system is busy. Please try again later.
For any further questions, feel free to contact us through the chatbot.
Chatbot