mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
python docs, AI bot rough start
This commit is contained in:
parent
445369fe42
commit
de707ad953
@ -18,6 +18,9 @@
|
||||
"clear-docs": "node scripts/clear-docs.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/google": "^1.2.21",
|
||||
"@ai-sdk/openai": "^1.3.22",
|
||||
"@ai-sdk/react": "^1.2.12",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-presence": "^1.1.4",
|
||||
@ -26,6 +29,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@stackframe/stack": "workspace:^",
|
||||
"@xyflow/react": "^12.6.4",
|
||||
"ai": "^4.3.16",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"fumadocs-core": "15.3.3",
|
||||
"fumadocs-mdx": "11.6.4",
|
||||
@ -45,7 +49,8 @@
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx": "^3.1.0",
|
||||
"shiki": "^3.4.2",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import AIChat from '@/components/chat/ai-chat';
|
||||
import DocsSelector from '@/components/homepage/iconHover';
|
||||
|
||||
export default function HomePage() {
|
||||
@ -34,6 +35,11 @@ export default function HomePage() {
|
||||
<div className="mb-6">
|
||||
<DocsSelector />
|
||||
</div>
|
||||
|
||||
{/* AI Chat Assistant */}
|
||||
<div className="mb-6">
|
||||
<AIChat />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
119
docs/src/app/api/chat/route.ts
Normal file
119
docs/src/app/api/chat/route.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { streamText } from 'ai';
|
||||
|
||||
// Allow streaming responses up to 30 seconds
|
||||
export const maxDuration = 30;
|
||||
|
||||
// Configure Google Gemini with custom API key variable
|
||||
const google = createGoogleGenerativeAI({
|
||||
apiKey: process.env.GOOGLE_AI_API_KEY,
|
||||
});
|
||||
|
||||
export function errorHandler(error: unknown) {
|
||||
if (error == null) {
|
||||
return 'unknown error';
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
|
||||
async function getStackAuthDocs() {
|
||||
try {
|
||||
// Get the base URL from the request or use localhost for development
|
||||
const baseUrl = process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: 'http://localhost:8104';
|
||||
|
||||
console.log('Fetching docs from:', `${baseUrl}/llms.txt`);
|
||||
|
||||
const response = await fetch(`${baseUrl}/llms.txt`);
|
||||
console.log('Docs fetch response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch Stack Auth docs:', response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
const docsContent = await response.text();
|
||||
console.log('Docs content length:', docsContent?.length || 0);
|
||||
console.log('Docs content preview:', docsContent?.substring(0, 200) + '...');
|
||||
|
||||
return docsContent;
|
||||
} catch (error) {
|
||||
console.error('Error fetching Stack Auth docs:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { messages } = await req.json();
|
||||
|
||||
console.log('Received messages:', messages);
|
||||
console.log('Google AI API Key configured:', !!process.env.GOOGLE_AI_API_KEY);
|
||||
|
||||
// Fetch Stack Auth documentation
|
||||
const stackAuthDocs = await getStackAuthDocs();
|
||||
|
||||
// Create system message with documentation context
|
||||
const systemMessage = {
|
||||
role: 'system' as const,
|
||||
content: `You are a technical AI assistant specializing in Stack Auth, a complete authentication solution. You are helping developers who want detailed, technical guidance.
|
||||
|
||||
IMPORTANT INSTRUCTIONS:
|
||||
- You can ONLY answer questions about Stack Auth and authentication topics
|
||||
- If someone asks about anything else, politely redirect them to ask about Stack Auth
|
||||
- Your audience is DEVELOPERS who need in-depth technical information
|
||||
- Provide comprehensive, detailed answers with code examples when available
|
||||
- Include specific implementation details, configuration options, and best practices
|
||||
- Reference exact function names, parameters, and code snippets from the documentation
|
||||
- Don't oversimplify - developers want the full technical depth
|
||||
- When explaining concepts, include relevant code examples and implementation details
|
||||
- If there are multiple approaches, explain the different options and their trade-offs
|
||||
|
||||
${stackAuthDocs ? `
|
||||
Here is the complete Stack Auth documentation with detailed examples and technical information:
|
||||
|
||||
${stackAuthDocs}
|
||||
|
||||
Use this documentation to provide comprehensive, technical answers. Include code examples, configuration details, and implementation specifics. Developers are looking for actionable, detailed guidance, not basic overviews.
|
||||
` : 'Stack Auth documentation could not be loaded. Please answer based on general Stack Auth knowledge, but provide detailed technical information for developers.'}
|
||||
|
||||
Remember: Your responses should match the technical depth and detail level of the Stack Auth documentation. Provide code examples, configuration snippets, and comprehensive implementation guidance.`
|
||||
};
|
||||
|
||||
// Prepend system message to the conversation
|
||||
const messagesWithContext = [systemMessage, ...messages];
|
||||
|
||||
const result = streamText({
|
||||
model: google('gemini-1.5-flash'),
|
||||
messages: messagesWithContext,
|
||||
});
|
||||
|
||||
return result.toDataStreamResponse({
|
||||
getErrorMessage: errorHandler,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in chat API:', error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
120
docs/src/components/chat/ai-chat.tsx
Normal file
120
docs/src/components/chat/ai-chat.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { Bot, Send, User } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function AIChat() {
|
||||
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Debug logging
|
||||
console.log('Messages:', messages);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto">
|
||||
{/* Toggle Button */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-1 font-semibold"
|
||||
>
|
||||
<Bot size={20} />
|
||||
{isOpen ? 'Hide AI Assistant' : 'Ask AI Assistant'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Chat Interface */}
|
||||
{isOpen && (
|
||||
<div className="bg-card border border-border rounded-xl shadow-lg p-6 mb-8">
|
||||
<div className="flex items-center gap-2 mb-4 pb-4 border-b border-border">
|
||||
<Bot size={24} className="text-primary" />
|
||||
<h3 className="text-lg font-semibold">Stack Auth AI Assistant</h3>
|
||||
<span className="text-sm text-muted-foreground ml-auto">
|
||||
Ask questions about Stack Auth documentation
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Messages Container */}
|
||||
<div className="h-96 overflow-y-auto mb-4 space-y-4 scroll-smooth">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<Bot size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium mb-2">Welcome to Stack Auth!</p>
|
||||
<p className="text-sm">
|
||||
Ask me anything about authentication, documentation, or how to get started.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map(message => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex gap-3 ${
|
||||
message.role === 'user' ? 'justify-end' : 'justify-start'
|
||||
}`}
|
||||
>
|
||||
{message.role === 'assistant' && (
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<Bot size={16} className="text-primary" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`max-w-[80%] rounded-xl px-4 py-3 ${
|
||||
message.role === 'user'
|
||||
? 'bg-primary text-primary-foreground ml-auto'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
<div className="whitespace-pre-wrap text-sm leading-relaxed">
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.role === 'user' && (
|
||||
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<User size={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex gap-3 justify-start">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<Bot size={16} className="text-primary" />
|
||||
</div>
|
||||
<div className="bg-muted rounded-xl px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce" />
|
||||
<div className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
|
||||
<div className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Form */}
|
||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 px-4 py-3 bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent placeholder:text-muted-foreground"
|
||||
value={input}
|
||||
placeholder="Ask about Stack Auth documentation..."
|
||||
onChange={handleInputChange}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="px-4 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
|
||||
>
|
||||
<Send size={16} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -227,259 +227,74 @@ const DocsIcon3D: React.FC<DocsIcon3DProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Add custom CSS for the floating animation
|
||||
const floatingDotsStyle = `
|
||||
@keyframes float-up {
|
||||
0% {
|
||||
transform: translateY(0px) scale(1);
|
||||
opacity: 0.8;
|
||||
filter: blur(0px);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
filter: blur(0.5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-200px) scale(0.3);
|
||||
opacity: 0;
|
||||
filter: blur(1px);
|
||||
}
|
||||
}
|
||||
.animate-float-up {
|
||||
animation: float-up 3s ease-out infinite;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: floatingDotsStyle }} />
|
||||
<div className="flex justify-center items-center p-4">
|
||||
<div
|
||||
className={`
|
||||
grid gap-4 max-w-4xl w-full justify-center
|
||||
${platformSections.length === 1 ? 'grid-cols-1 max-w-xs' : ''}
|
||||
${platformSections.length === 2 ? 'grid-cols-1 md:grid-cols-2 max-w-lg' : ''}
|
||||
${platformSections.length === 3 ? 'grid-cols-1 md:grid-cols-3 max-w-2xl' : ''}
|
||||
${platformSections.length === 4 ? 'grid-cols-2 md:grid-cols-4' : ''}
|
||||
`}
|
||||
>
|
||||
{platformSections.map((section) => (
|
||||
<div className="flex justify-center items-center p-4">
|
||||
<div
|
||||
className={`
|
||||
grid gap-6 max-w-4xl w-full justify-center
|
||||
${platformSections.length === 1 ? 'grid-cols-1 max-w-xs' : ''}
|
||||
${platformSections.length === 2 ? 'grid-cols-1 md:grid-cols-2 max-w-lg' : ''}
|
||||
${platformSections.length === 3 ? 'grid-cols-1 md:grid-cols-3 max-w-2xl' : ''}
|
||||
${platformSections.length === 4 ? 'grid-cols-2 md:grid-cols-4' : ''}
|
||||
`}
|
||||
>
|
||||
{platformSections.map((section) => (
|
||||
<div
|
||||
key={section.id}
|
||||
className="cursor-pointer group transform transition-all duration-200 hover:scale-105"
|
||||
onMouseEnter={() => setHoveredSection(section.id)}
|
||||
onMouseLeave={() => setHoveredSection(null)}
|
||||
onClick={() => handleSectionClick(section)}
|
||||
>
|
||||
<div
|
||||
key={section.id}
|
||||
className={`
|
||||
relative cursor-pointer group
|
||||
transform transition-all duration-500 ease-out
|
||||
hover:scale-105 hover:-translate-y-2 hover:rotate-1
|
||||
active:scale-95 active:rotate-0
|
||||
`}
|
||||
onMouseEnter={() => setHoveredSection(section.id)}
|
||||
onMouseLeave={() => setHoveredSection(null)}
|
||||
onClick={() => handleSectionClick(section)}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
relative bg-gradient-to-br from-background via-background to-muted/20
|
||||
border-2 border-border rounded-2xl p-4 w-full h-44
|
||||
bg-card border-[0.5px] border-border rounded-xl p-6 w-full h-40
|
||||
flex flex-col items-center justify-center
|
||||
shadow-xl hover:shadow-2xl
|
||||
overflow-hidden backdrop-blur-sm
|
||||
transition-all duration-500 ease-out
|
||||
before:absolute before:inset-0 before:bg-gradient-to-br before:opacity-0
|
||||
hover:before:opacity-100 before:transition-opacity before:duration-500
|
||||
${hoveredSection === section.id ? "border-opacity-100 shadow-2xl" : "border-opacity-50"}
|
||||
shadow-sm hover:shadow-lg
|
||||
transition-all duration-200
|
||||
`}
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
perspective: "1000px",
|
||||
borderColor: hoveredSection === section.id ? section.color : undefined,
|
||||
boxShadow:
|
||||
hoveredSection === section.id
|
||||
? `0 25px 50px -12px ${section.color}40, 0 0 0 1px ${section.color}20`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{/* Animated background gradient */}
|
||||
style={{
|
||||
borderColor: hoveredSection === section.id ? section.color : undefined,
|
||||
}}
|
||||
>
|
||||
{/* Icon Container */}
|
||||
<div className="mb-4">
|
||||
<div
|
||||
className={`
|
||||
absolute inset-0 rounded-2xl transition-opacity duration-500
|
||||
${hoveredSection === section.id ? "opacity-100" : "opacity-0"}
|
||||
`}
|
||||
w-12 h-12 rounded-lg flex items-center justify-center
|
||||
transition-all duration-200
|
||||
`}
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(circle at 30% 20%, ${section.color}15 0%, transparent 50%),
|
||||
linear-gradient(135deg, ${section.color}08, ${section.color}03, transparent)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Continuous upward floating dots effect */}
|
||||
<div className="absolute inset-0 overflow-hidden rounded-2xl">
|
||||
{hoveredSection === section.id && (
|
||||
<div className="absolute inset-0">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div
|
||||
key={`${section.id}-${i}`}
|
||||
className="absolute w-1.5 h-1.5 rounded-full animate-float-up"
|
||||
style={{
|
||||
backgroundColor: section.color,
|
||||
left: `${10 + Math.random() * 80}%`,
|
||||
bottom: "-6px",
|
||||
animationDelay: `${i * 0.3}s`,
|
||||
animationDuration: "3s",
|
||||
animationIterationCount: "infinite",
|
||||
animationTimingFunction: "ease-out",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 3D Icon Container */}
|
||||
<div
|
||||
className={`
|
||||
relative mb-4 transition-all duration-700 ease-out
|
||||
${
|
||||
hoveredSection === section.id
|
||||
? "transform -rotate-y-12 rotate-x-6 translate-z-8"
|
||||
: "transform rotate-0"
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
backgroundColor: hoveredSection === section.id ? section.color : `${section.color}20`,
|
||||
color: hoveredSection === section.id ? 'white' : 'hsl(var(--foreground))',
|
||||
}}
|
||||
>
|
||||
{/* Enhanced shadow layers */}
|
||||
<div
|
||||
className={`
|
||||
absolute inset-0 rounded-xl transition-all duration-500
|
||||
${hoveredSection === section.id ? "opacity-50 blur-sm" : "opacity-20"}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: section.color,
|
||||
transform: "translateZ(-6px) translateX(3px) translateY(3px)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Main icon with enhanced styling */}
|
||||
<div
|
||||
className={`
|
||||
relative z-10 w-16 h-16 rounded-xl flex items-center justify-center
|
||||
border-2 transition-all duration-500 ease-out
|
||||
${
|
||||
hoveredSection === section.id
|
||||
? "text-white border-transparent scale-110 rotate-3"
|
||||
: "text-foreground border-border bg-background scale-100"
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: hoveredSection === section.id ? section.color : undefined,
|
||||
boxShadow:
|
||||
hoveredSection === section.id
|
||||
? `0 12px 40px ${section.color}60, inset 0 1px 0 rgba(255,255,255,0.2)`
|
||||
: `0 6px 25px ${section.color}30`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
transition-all duration-500 ease-out
|
||||
${hoveredSection === section.id ? "scale-125 rotate-6" : "scale-100"}
|
||||
`}
|
||||
>
|
||||
{React.cloneElement(section.icon as React.ReactElement, {
|
||||
size: 24,
|
||||
strokeWidth: hoveredSection === section.id ? 2.5 : 2,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Title */}
|
||||
<h3
|
||||
className={`
|
||||
text-lg font-bold mb-2 text-center transition-all duration-500
|
||||
${hoveredSection === section.id ? "scale-105 font-extrabold" : "scale-100"}
|
||||
`}
|
||||
style={{
|
||||
color: hoveredSection === section.id ? section.color : undefined,
|
||||
textShadow: hoveredSection === section.id ? `0 0 20px ${section.color}40` : undefined,
|
||||
}}
|
||||
>
|
||||
{section.title}
|
||||
</h3>
|
||||
|
||||
{/* Enhanced Description */}
|
||||
<p
|
||||
className={`
|
||||
text-xs text-center leading-relaxed px-2 transition-all duration-500
|
||||
${
|
||||
hoveredSection === section.id
|
||||
? "text-muted-foreground opacity-100 scale-105"
|
||||
: "text-muted-foreground opacity-70"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{section.description}
|
||||
</p>
|
||||
|
||||
{/* Enhanced hover indicator */}
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-0 left-0 right-0 h-1.5 rounded-b-2xl
|
||||
transition-all duration-500 ease-out
|
||||
${hoveredSection === section.id ? "scale-x-100" : "scale-x-0"}
|
||||
`}
|
||||
style={{
|
||||
transformOrigin: "left",
|
||||
backgroundColor: section.color,
|
||||
boxShadow: hoveredSection === section.id ? `0 0 20px ${section.color}60` : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Enhanced corner accents with glow */}
|
||||
<div
|
||||
className={`
|
||||
absolute top-3 right-3 w-2 h-2 rounded-full
|
||||
transition-all duration-500 ease-out
|
||||
${hoveredSection === section.id ? "scale-150 opacity-100" : "scale-75 opacity-30"}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: section.color,
|
||||
boxShadow: hoveredSection === section.id ? `0 0 15px ${section.color}80` : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-3 left-3 w-2 h-2 rounded-full
|
||||
transition-all duration-500 ease-out
|
||||
${hoveredSection === section.id ? "scale-150 opacity-100" : "scale-75 opacity-30"}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: section.color,
|
||||
boxShadow: hoveredSection === section.id ? `0 0 15px ${section.color}80` : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* New: Diagonal accent line */}
|
||||
<div
|
||||
className={`
|
||||
absolute top-0 right-0 w-16 h-16 overflow-hidden rounded-tr-2xl
|
||||
transition-all duration-500
|
||||
${hoveredSection === section.id ? "opacity-100" : "opacity-0"}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 right-0 w-full h-0.5 origin-top-right rotate-45 translate-x-2 translate-y-4"
|
||||
style={{ backgroundColor: section.color }}
|
||||
/>
|
||||
{React.cloneElement(section.icon as React.ReactElement, {
|
||||
size: 20,
|
||||
strokeWidth: 2,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3
|
||||
className="text-sm font-semibold mb-2 text-center transition-all duration-200"
|
||||
style={{
|
||||
color: hoveredSection === section.id ? section.color : 'hsl(var(--card-foreground))',
|
||||
}}
|
||||
>
|
||||
{section.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-xs text-center text-muted-foreground leading-relaxed px-2 opacity-80">
|
||||
{section.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Code, FileText, Link as LinkIcon, Play, Puzzle, Settings, Shield, UserCheck } from 'lucide-react';
|
||||
import { Code, FileText, Link as LinkIcon, Play, Puzzle, Settings, Shield, User, UserCheck } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { type ReactNode } from 'react';
|
||||
import { cn } from '../../lib/cn';
|
||||
@ -17,6 +17,7 @@ const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
'user-check': UserCheck,
|
||||
'link': LinkIcon,
|
||||
'shield-check': Shield,
|
||||
'user': User,
|
||||
};
|
||||
|
||||
export type CardProps = {
|
||||
|
||||
@ -1,165 +0,0 @@
|
||||
---
|
||||
title: API Setup & Configuration
|
||||
---
|
||||
|
||||
This guide covers the essential setup for using Stack Auth's REST API in your Python application. Stack Auth provides a REST API for managing users, sessions, and authentication flows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, make sure you have:
|
||||
- A Stack Auth [project](https://app.stack-auth.com/projects) created
|
||||
- Python 3.7+ installed
|
||||
- `requests` library (`pip install requests`)
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
First, set up your API credentials. Get these from your Stack Auth dashboard:
|
||||
|
||||
```bash
|
||||
# .env file
|
||||
STACK_PROJECT_ID=your_project_id
|
||||
STACK_PUBLISHABLE_KEY=your_publishable_key
|
||||
STACK_SECRET_KEY=your_secret_key
|
||||
STACK_API_URL=https://api.stack-auth.com
|
||||
```
|
||||
|
||||
## Basic API Client Setup
|
||||
|
||||
Create a basic API client to handle Stack Auth requests:
|
||||
|
||||
```python
|
||||
import os
|
||||
import requests
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class StackAuthClient:
|
||||
def __init__(self):
|
||||
self.project_id = os.getenv('STACK_PROJECT_ID')
|
||||
self.secret_key = os.getenv('STACK_SECRET_KEY')
|
||||
self.publishable_key = os.getenv('STACK_PUBLISHABLE_KEY')
|
||||
self.api_url = os.getenv('STACK_API_URL', 'https://api.stack-auth.com')
|
||||
|
||||
if not all([self.project_id, self.secret_key]):
|
||||
raise ValueError("Missing required Stack Auth credentials")
|
||||
|
||||
def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
) -> requests.Response:
|
||||
"""Make authenticated request to Stack Auth API"""
|
||||
url = f"{self.api_url}/api/v1{endpoint}"
|
||||
|
||||
# Add authentication headers
|
||||
auth_headers = {
|
||||
'X-Stack-Project-Id': self.project_id,
|
||||
'X-Stack-Secret-Key': self.secret_key,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if headers:
|
||||
auth_headers.update(headers)
|
||||
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
json=data,
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
# Handle common error cases
|
||||
if response.status_code == 401:
|
||||
raise Exception("Authentication failed - check your API credentials")
|
||||
elif response.status_code == 403:
|
||||
raise Exception("Forbidden - insufficient permissions")
|
||||
elif not response.ok:
|
||||
raise Exception(f"API request failed: {response.status_code} - {response.text}")
|
||||
|
||||
return response
|
||||
|
||||
def get(self, endpoint: str, **kwargs) -> requests.Response:
|
||||
return self._make_request('GET', endpoint, **kwargs)
|
||||
|
||||
def post(self, endpoint: str, data: Dict[str, Any] = None, **kwargs) -> requests.Response:
|
||||
return self._make_request('POST', endpoint, data, **kwargs)
|
||||
|
||||
def put(self, endpoint: str, data: Dict[str, Any] = None, **kwargs) -> requests.Response:
|
||||
return self._make_request('PUT', endpoint, data, **kwargs)
|
||||
|
||||
def delete(self, endpoint: str, **kwargs) -> requests.Response:
|
||||
return self._make_request('DELETE', endpoint, **kwargs)
|
||||
|
||||
# Initialize the client
|
||||
stack_client = StackAuthClient()
|
||||
```
|
||||
|
||||
## Testing Your Setup
|
||||
|
||||
Test your API connection with a simple request:
|
||||
|
||||
```python
|
||||
def test_connection():
|
||||
try:
|
||||
# Test with a simple API call
|
||||
response = stack_client.get('/users')
|
||||
print("✅ Connection successful!")
|
||||
print(f"Found {len(response.json().get('users', []))} users")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Connection failed: {e}")
|
||||
return False
|
||||
|
||||
# Run the test
|
||||
if __name__ == "__main__":
|
||||
test_connection()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Implement proper error handling for production use:
|
||||
|
||||
```python
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StackAuthError(Exception):
|
||||
"""Base exception for Stack Auth errors"""
|
||||
pass
|
||||
|
||||
class StackAuthClient:
|
||||
# ... previous code ...
|
||||
|
||||
def safe_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Make a safe request with comprehensive error handling"""
|
||||
try:
|
||||
response = self._make_request(method, endpoint, data)
|
||||
return response.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error("Failed to connect to Stack Auth API")
|
||||
raise StackAuthError("Network connection failed")
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("Request to Stack Auth API timed out")
|
||||
raise StackAuthError("Request timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"Stack Auth API error: {e}")
|
||||
raise StackAuthError(f"API request failed: {e}")
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
With your API client set up, you can now:
|
||||
|
||||
- [Authenticate users](./user-sessions) with your application
|
||||
- [Handle OAuth flows](./oauth-flows) for social login
|
||||
- [Validate server-side sessions](./server-validation)
|
||||
|
||||
For detailed API reference, see the [REST API documentation](/api/overview).
|
||||
@ -1,56 +1,252 @@
|
||||
---
|
||||
title: "Authentication Flows"
|
||||
description: "Learn how to implement authentication flows in your Python application using Stack Auth's REST API"
|
||||
title: Authentication
|
||||
---
|
||||
This section covers the core authentication patterns and flows you'll need to implement secure authentication in your Python application using Stack Auth's REST API.
|
||||
|
||||
## Core Authentication
|
||||
This guide covers how to implement authentication in your Python application using Stack Auth. We'll walk through all the available authentication methods and show you how to handle user sessions.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="API Setup & Configuration"
|
||||
href="./authentication/api-setup"
|
||||
icon="settings"
|
||||
>
|
||||
Set up your Python client and configure API credentials for Stack Auth integration.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="User Sessions"
|
||||
href="./user-sessions"
|
||||
icon="user-check"
|
||||
>
|
||||
Handle user session management, validation, and lifecycle in Python applications.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="OAuth Flows"
|
||||
href="./oauth-flows"
|
||||
icon="link"
|
||||
>
|
||||
Implement OAuth authentication flows for social login providers via the REST API.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Server Validation"
|
||||
href="./server-validation"
|
||||
icon="shield-check"
|
||||
>
|
||||
Validate user tokens and authenticate requests on your Python backend.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
## Overview
|
||||
|
||||
## Authentication Patterns
|
||||
Stack Auth supports multiple authentication methods that you can enable or disable based on your needs:
|
||||
|
||||
Each authentication flow addresses specific use cases:
|
||||
- **[Email/Password](./password.mdx)** - Traditional email and password authentication
|
||||
- **[Magic Links (OTP)](./magic-links.mdx)** - Passwordless authentication via email
|
||||
- **[OAuth Providers](./oauth.mdx)** - Sign in with Google, GitHub, Facebook, and more
|
||||
- **[Passkeys](./passkeys.mdx)** - Modern biometric authentication
|
||||
- **[Anonymous Users](./anonymous.mdx)** - Guest users that can be upgraded later
|
||||
|
||||
- **API Setup**: Foundation for all authentication operations
|
||||
- **User Sessions**: Managing authenticated user state and session lifecycle
|
||||
- **OAuth Flows**: Social login integration (Google, GitHub, etc.)
|
||||
- **Server Validation**: Securing your API endpoints and validating requests
|
||||
## Basic Authentication Flow
|
||||
|
||||
## Getting Started
|
||||
All authentication methods in Stack Auth follow a similar pattern:
|
||||
|
||||
Start with [API Setup & Configuration](./api-setup) to establish your Python client, then proceed to the specific authentication flow that matches your application's needs.
|
||||
1. **Initiate authentication** - Start the sign-in/sign-up process
|
||||
2. **User verification** - User completes authentication (password, email link, OAuth, etc.)
|
||||
3. **Receive tokens** - Get access and refresh tokens
|
||||
4. **Manage session** - Use tokens to authenticate API requests
|
||||
|
||||
For framework-specific integration examples, see the [Framework Integration](../integration) section.
|
||||
Here's the basic structure for handling authentication:
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
class AuthManager:
|
||||
def __init__(self, stack_auth_client):
|
||||
self.client = stack_auth_client
|
||||
self.current_user_token: Optional[str] = None
|
||||
|
||||
def authenticate_request(self, access_token: str) -> dict:
|
||||
"""Verify and get user info from access token"""
|
||||
return self.client._make_request(
|
||||
'GET',
|
||||
'/api/v1/users/me',
|
||||
access_type="client",
|
||||
access_token=access_token
|
||||
)
|
||||
|
||||
def refresh_token(self, refresh_token: str) -> dict:
|
||||
"""Refresh an expired access token"""
|
||||
return self.client._make_request(
|
||||
'POST',
|
||||
'/api/v1/auth/sessions/current/refresh',
|
||||
access_type="client",
|
||||
json={'refresh_token': refresh_token}
|
||||
)
|
||||
|
||||
def sign_out(self, access_token: str) -> bool:
|
||||
"""Sign out the current user"""
|
||||
try:
|
||||
self.client._make_request(
|
||||
'DELETE',
|
||||
'/api/v1/auth/sessions/current',
|
||||
access_type="client",
|
||||
access_token=access_token
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Initialize auth manager
|
||||
auth_manager = AuthManager(stack_auth)
|
||||
```
|
||||
|
||||
## Token Management
|
||||
|
||||
Stack Auth uses two types of tokens:
|
||||
|
||||
- **Access Token** - Short-lived token for API requests (typically 1 hour)
|
||||
- **Refresh Token** - Long-lived token to get new access tokens (typically 30 days)
|
||||
|
||||
### Storing Tokens Securely
|
||||
|
||||
For web applications, store tokens securely:
|
||||
|
||||
```python
|
||||
from flask import session
|
||||
import jwt
|
||||
from datetime import datetime
|
||||
|
||||
def store_tokens_securely(access_token: str, refresh_token: str):
|
||||
"""Store tokens in secure HTTP-only cookies or session"""
|
||||
# For session storage
|
||||
session['access_token'] = access_token
|
||||
session['refresh_token'] = refresh_token
|
||||
|
||||
# For cookie storage (recommended for web apps)
|
||||
# Set secure, HTTP-only cookies
|
||||
response.set_cookie(
|
||||
'access_token',
|
||||
access_token,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite='Strict'
|
||||
)
|
||||
|
||||
def get_current_user_from_token(access_token: str) -> Optional[dict]:
|
||||
"""Extract user info from access token"""
|
||||
try:
|
||||
# Verify token and get user
|
||||
user_info = auth_manager.authenticate_request(access_token)
|
||||
return user_info
|
||||
except Exception:
|
||||
return None
|
||||
```
|
||||
|
||||
## Middleware for Authentication
|
||||
|
||||
Here's how to create middleware that automatically handles authentication:
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from flask import request, jsonify, g
|
||||
|
||||
def require_auth(f):
|
||||
"""Decorator that requires authentication"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# Get token from header or cookie
|
||||
access_token = request.headers.get('Authorization')
|
||||
if access_token and access_token.startswith('Bearer '):
|
||||
access_token = access_token[7:]
|
||||
elif 'access_token' in request.cookies:
|
||||
access_token = request.cookies['access_token']
|
||||
else:
|
||||
return jsonify({'error': 'Authentication required'}), 401
|
||||
|
||||
# Verify token and get user
|
||||
try:
|
||||
user_info = auth_manager.authenticate_request(access_token)
|
||||
g.current_user = user_info
|
||||
g.access_token = access_token
|
||||
return f(*args, **kwargs)
|
||||
except Exception as e:
|
||||
return jsonify({'error': 'Invalid token'}), 401
|
||||
|
||||
return decorated_function
|
||||
|
||||
def optional_auth(f):
|
||||
"""Decorator for optional authentication"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
access_token = request.headers.get('Authorization')
|
||||
if access_token and access_token.startswith('Bearer '):
|
||||
access_token = access_token[7:]
|
||||
try:
|
||||
user_info = auth_manager.authenticate_request(access_token)
|
||||
g.current_user = user_info
|
||||
g.access_token = access_token
|
||||
except Exception:
|
||||
g.current_user = None
|
||||
else:
|
||||
g.current_user = None
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Protected Route
|
||||
|
||||
```python
|
||||
from flask import Flask, g, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/profile')
|
||||
@require_auth
|
||||
def get_profile():
|
||||
"""Get current user's profile - requires authentication"""
|
||||
return jsonify({
|
||||
'user': g.current_user,
|
||||
'message': 'This is your profile'
|
||||
})
|
||||
|
||||
@app.route('/public')
|
||||
@optional_auth
|
||||
def public_endpoint():
|
||||
"""Public endpoint with optional user context"""
|
||||
if g.current_user:
|
||||
return jsonify({
|
||||
'message': f'Hello {g.current_user.get("display_name", "User")}!'
|
||||
})
|
||||
else:
|
||||
return jsonify({'message': 'Hello anonymous user!'})
|
||||
```
|
||||
|
||||
### Manual Token Refresh
|
||||
|
||||
```python
|
||||
def refresh_user_session(refresh_token: str) -> Optional[dict]:
|
||||
"""Refresh user session and return new tokens"""
|
||||
try:
|
||||
token_response = auth_manager.refresh_token(refresh_token)
|
||||
return {
|
||||
'access_token': token_response['access_token'],
|
||||
'refresh_token': token_response['refresh_token']
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Token refresh failed: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Common authentication errors and how to handle them:
|
||||
|
||||
```python
|
||||
class AuthError(Exception):
|
||||
def __init__(self, message: str, status_code: int = 401):
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
super().__init__(self.message)
|
||||
|
||||
def handle_auth_errors(func):
|
||||
"""Decorator to handle common authentication errors"""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
raise AuthError("Invalid or expired token", 401)
|
||||
elif e.response.status_code == 403:
|
||||
raise AuthError("Insufficient permissions", 403)
|
||||
else:
|
||||
raise AuthError("Authentication error", e.response.status_code)
|
||||
except Exception as e:
|
||||
raise AuthError(f"Unexpected error: {str(e)}", 500)
|
||||
return wrapper
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Choose the authentication method that best fits your application:
|
||||
|
||||
1. **[Email/Password Authentication](./password.mdx)** - Most common, familiar to users
|
||||
2. **[Magic Link Authentication](./magic-links.mdx)** - Modern passwordless approach
|
||||
3. **[OAuth Authentication](./oauth.mdx)** - Easy sign-in with existing accounts
|
||||
4. **[Passkey Authentication](./passkeys.mdx)** - Most secure, future-proof option
|
||||
5. **[Anonymous Authentication](./anonymous.mdx)** - Great for guest users
|
||||
|
||||
You can also enable multiple methods simultaneously to give users options.
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"title": "Authentication",
|
||||
"description": "Authentication flows and patterns for Python applications",
|
||||
"pages": [
|
||||
"index",
|
||||
"api-setup",
|
||||
"user-sessions",
|
||||
"oauth-flows",
|
||||
|
||||
378
docs/templates-python/authentication/user-authentication.mdx
Normal file
378
docs/templates-python/authentication/user-authentication.mdx
Normal file
@ -0,0 +1,378 @@
|
||||
---
|
||||
title: "User Authentication"
|
||||
description: "Learn how to implement user authentication in your Python application using Stack Auth's REST API"
|
||||
---
|
||||
|
||||
After creating a [helper function](../getting-started/setup.mdx) to make requests to the Stack Auth API, you can start using the API to authenticate users.
|
||||
|
||||
## User Authentication
|
||||
|
||||
Stack Auth supports multiple authentication methods:
|
||||
- **Password Authentication** - Email and password
|
||||
- **OTP Authentication** - Magic links and one-time passwords via email
|
||||
- **OAuth Authentication** - Social logins (GitHub, Google, etc.)
|
||||
- **Passkey Authentication** - WebAuthn/FIDO2 passkeys
|
||||
- **Multi-Factor Authentication** - TOTP-based MFA
|
||||
|
||||
### Sign Up with Email and Password
|
||||
|
||||
To create a new user account with email and password:
|
||||
|
||||
```python
|
||||
def sign_up_with_password(email, password, verification_callback_url):
|
||||
"""
|
||||
Sign up a new user with email and password
|
||||
Returns access_token, refresh_token, and user_id
|
||||
"""
|
||||
response = stack_auth_request('POST', 'api/v1/auth/password/sign-up', json={
|
||||
'email': email,
|
||||
'password': password,
|
||||
'verification_callback_url': verification_callback_url # URL where user will verify email
|
||||
})
|
||||
|
||||
return {
|
||||
'access_token': response['access_token'],
|
||||
'refresh_token': response['refresh_token'],
|
||||
'user_id': response['user_id']
|
||||
}
|
||||
|
||||
# Example usage
|
||||
user_data = sign_up_with_password(
|
||||
email="user@example.com",
|
||||
password="secure_password_123",
|
||||
verification_callback_url="https://yourapp.com/verify-email"
|
||||
)
|
||||
```
|
||||
|
||||
### Sign In with Email and Password
|
||||
|
||||
To authenticate an existing user:
|
||||
|
||||
```python
|
||||
def sign_in_with_password(email, password):
|
||||
"""
|
||||
Sign in an existing user with email and password
|
||||
Returns access_token, refresh_token, and user_id
|
||||
"""
|
||||
response = stack_auth_request('POST', 'api/v1/auth/password/sign-in', json={
|
||||
'email': email,
|
||||
'password': password
|
||||
})
|
||||
|
||||
return {
|
||||
'access_token': response['access_token'],
|
||||
'refresh_token': response['refresh_token'],
|
||||
'user_id': response['user_id']
|
||||
}
|
||||
|
||||
# Example usage
|
||||
user_data = sign_in_with_password("user@example.com", "secure_password_123")
|
||||
access_token = user_data['access_token']
|
||||
refresh_token = user_data['refresh_token']
|
||||
```
|
||||
|
||||
### Sign In with OTP (Magic Link)
|
||||
|
||||
For passwordless authentication using one-time passwords:
|
||||
|
||||
```python
|
||||
def send_otp_code(email, callback_url):
|
||||
"""
|
||||
Send an OTP code to the user's email
|
||||
Returns a nonce that must be stored for verification
|
||||
"""
|
||||
response = stack_auth_request('POST', 'api/v1/auth/otp/send-sign-in-code', json={
|
||||
'email': email,
|
||||
'callback_url': callback_url # URL where user will complete sign-in
|
||||
})
|
||||
|
||||
return response['nonce']
|
||||
|
||||
def verify_otp_code(nonce, six_digit_code):
|
||||
"""
|
||||
Verify the OTP code and complete sign-in
|
||||
The code parameter should be the 6-digit code + nonce concatenated
|
||||
Returns access_token, refresh_token, and user_id
|
||||
"""
|
||||
# The verification code is the 6-digit code followed by the nonce
|
||||
verification_code = six_digit_code + nonce
|
||||
|
||||
response = stack_auth_request('POST', 'api/v1/auth/otp/sign-in', json={
|
||||
'code': verification_code
|
||||
})
|
||||
|
||||
return {
|
||||
'access_token': response['access_token'],
|
||||
'refresh_token': response['refresh_token'],
|
||||
'user_id': response['user_id'],
|
||||
'is_new_user': response['is_new_user'] # True if this was a sign-up
|
||||
}
|
||||
|
||||
# Example usage
|
||||
nonce = send_otp_code("user@example.com", "https://yourapp.com/verify-otp")
|
||||
# Store the nonce temporarily, user receives email with 6-digit code
|
||||
# When user enters the code:
|
||||
user_data = verify_otp_code(nonce, "123456")
|
||||
```
|
||||
|
||||
### Get Current User Information
|
||||
|
||||
To retrieve information about the currently authenticated user:
|
||||
|
||||
```python
|
||||
def get_current_user(access_token):
|
||||
"""
|
||||
Get the current user's information using their access token
|
||||
"""
|
||||
response = stack_auth_request('GET', 'api/v1/users/me', headers={
|
||||
'x-stack-access-token': access_token
|
||||
})
|
||||
|
||||
return {
|
||||
'id': response['id'],
|
||||
'display_name': response['display_name'],
|
||||
'primary_email': response['primary_email'],
|
||||
'primary_email_verified': response['primary_email_verified'],
|
||||
'profile_image_url': response['profile_image_url'],
|
||||
'signed_up_at_millis': response['signed_up_at_millis'],
|
||||
'last_active_at_millis': response['last_active_at_millis'],
|
||||
'oauth_providers': response['oauth_providers'],
|
||||
'has_password': response['has_password'],
|
||||
'auth_with_email': response['auth_with_email']
|
||||
}
|
||||
|
||||
# Example usage
|
||||
user_info = get_current_user(access_token)
|
||||
print(f"Welcome, {user_info['display_name']}!")
|
||||
```
|
||||
|
||||
### Refresh Access Token
|
||||
|
||||
Access tokens expire after a short time (typically 10 minutes). Use the refresh token to get a new access token:
|
||||
|
||||
```python
|
||||
def refresh_access_token(refresh_token):
|
||||
"""
|
||||
Get a new access token using the refresh token
|
||||
"""
|
||||
response = stack_auth_request('POST', 'api/v1/auth/sessions/current/refresh', headers={
|
||||
'x-stack-refresh-token': refresh_token
|
||||
})
|
||||
|
||||
return response['access_token']
|
||||
|
||||
# Example usage
|
||||
new_access_token = refresh_access_token(refresh_token)
|
||||
```
|
||||
|
||||
### Sign Out (Revoke Session)
|
||||
|
||||
To sign out a user by revoking their session:
|
||||
|
||||
```python
|
||||
def get_user_sessions(access_token):
|
||||
"""
|
||||
Get all active sessions for the current user
|
||||
"""
|
||||
response = stack_auth_request('GET', 'api/v1/auth/sessions', headers={
|
||||
'x-stack-access-token': access_token
|
||||
})
|
||||
|
||||
return response['items']
|
||||
|
||||
def sign_out_session(access_token, session_id):
|
||||
"""
|
||||
Sign out by deleting a specific session
|
||||
"""
|
||||
stack_auth_request('DELETE', f'api/v1/auth/sessions/{session_id}', headers={
|
||||
'x-stack-access-token': access_token
|
||||
})
|
||||
|
||||
def sign_out_current_user(access_token):
|
||||
"""
|
||||
Sign out the current user by finding and deleting their current session
|
||||
"""
|
||||
sessions = get_user_sessions(access_token)
|
||||
current_session = next((s for s in sessions if s['is_current_session']), None)
|
||||
|
||||
if current_session:
|
||||
# Note: This will fail with "CannotDeleteCurrentSession" error
|
||||
# Instead, you should invalidate the tokens on your client side
|
||||
pass
|
||||
|
||||
# In practice, you would typically just discard the tokens client-side
|
||||
print("User signed out (tokens should be discarded client-side)")
|
||||
|
||||
# Example usage
|
||||
sign_out_current_user(access_token)
|
||||
```
|
||||
|
||||
## Complete Authentication Flow Example
|
||||
|
||||
Here's a complete example that demonstrates a full authentication flow:
|
||||
|
||||
```python
|
||||
import os
|
||||
import requests
|
||||
|
||||
# Setup (from setup guide)
|
||||
stack_project_id = os.getenv("STACK_PROJECT_ID")
|
||||
stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY")
|
||||
stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")
|
||||
|
||||
def stack_auth_request(method, endpoint, **kwargs):
|
||||
res = requests.request(
|
||||
method,
|
||||
f'https://api.stack-auth.com/{endpoint}',
|
||||
headers={
|
||||
'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API
|
||||
'x-stack-project-id': stack_project_id,
|
||||
'x-stack-publishable-client-key': stack_publishable_client_key,
|
||||
'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client'
|
||||
**kwargs.pop('headers', {}),
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
if res.status_code >= 400:
|
||||
raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}")
|
||||
return res.json()
|
||||
|
||||
class StackAuthClient:
|
||||
def __init__(self):
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self.user_id = None
|
||||
|
||||
def sign_up(self, email, password, verification_callback_url):
|
||||
"""Sign up a new user"""
|
||||
response = stack_auth_request('POST', 'api/v1/auth/password/sign-up', json={
|
||||
'email': email,
|
||||
'password': password,
|
||||
'verification_callback_url': verification_callback_url
|
||||
})
|
||||
|
||||
self.access_token = response['access_token']
|
||||
self.refresh_token = response['refresh_token']
|
||||
self.user_id = response['user_id']
|
||||
|
||||
return response
|
||||
|
||||
def sign_in(self, email, password):
|
||||
"""Sign in an existing user"""
|
||||
response = stack_auth_request('POST', 'api/v1/auth/password/sign-in', json={
|
||||
'email': email,
|
||||
'password': password
|
||||
})
|
||||
|
||||
self.access_token = response['access_token']
|
||||
self.refresh_token = response['refresh_token']
|
||||
self.user_id = response['user_id']
|
||||
|
||||
return response
|
||||
|
||||
def get_current_user(self):
|
||||
"""Get current user information"""
|
||||
if not self.access_token:
|
||||
raise Exception("No access token available")
|
||||
|
||||
return stack_auth_request('GET', 'api/v1/users/me', headers={
|
||||
'x-stack-access-token': self.access_token
|
||||
})
|
||||
|
||||
def refresh_token_if_needed(self):
|
||||
"""Refresh the access token"""
|
||||
if not self.refresh_token:
|
||||
raise Exception("No refresh token available")
|
||||
|
||||
response = stack_auth_request('POST', 'api/v1/auth/sessions/current/refresh', headers={
|
||||
'x-stack-refresh-token': self.refresh_token
|
||||
})
|
||||
|
||||
self.access_token = response['access_token']
|
||||
return response
|
||||
|
||||
def sign_out(self):
|
||||
"""Sign out by clearing tokens"""
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self.user_id = None
|
||||
|
||||
# Example usage
|
||||
auth_client = StackAuthClient()
|
||||
|
||||
# Sign up a new user
|
||||
try:
|
||||
auth_client.sign_up(
|
||||
email="newuser@example.com",
|
||||
password="secure_password_123",
|
||||
verification_callback_url="https://yourapp.com/verify"
|
||||
)
|
||||
print("User signed up successfully!")
|
||||
except Exception as e:
|
||||
print(f"Sign up failed: {e}")
|
||||
|
||||
# Get user information
|
||||
try:
|
||||
user_info = auth_client.get_current_user()
|
||||
print(f"Logged in as: {user_info['primary_email']}")
|
||||
except Exception as e:
|
||||
print(f"Failed to get user info: {e}")
|
||||
|
||||
# Refresh token when needed
|
||||
try:
|
||||
auth_client.refresh_token_if_needed()
|
||||
print("Token refreshed successfully!")
|
||||
except Exception as e:
|
||||
print(f"Token refresh failed: {e}")
|
||||
|
||||
# Sign out
|
||||
auth_client.sign_out()
|
||||
print("User signed out!")
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Common errors you might encounter:
|
||||
|
||||
```python
|
||||
def handle_auth_errors(func):
|
||||
"""Decorator to handle common authentication errors"""
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
|
||||
if "EmailPasswordMismatch" in error_message:
|
||||
print("Invalid email or password")
|
||||
elif "AccessTokenExpired" in error_message:
|
||||
print("Access token expired, please refresh")
|
||||
elif "UserWithEmailAlreadyExists" in error_message:
|
||||
print("User with this email already exists")
|
||||
elif "PasswordAuthenticationNotEnabled" in error_message:
|
||||
print("Password authentication is not enabled for this project")
|
||||
else:
|
||||
print(f"Authentication error: {error_message}")
|
||||
|
||||
raise e
|
||||
return wrapper
|
||||
|
||||
@handle_auth_errors
|
||||
def safe_sign_in(email, password):
|
||||
return sign_in_with_password(email, password)
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you have user authentication working, you can:
|
||||
|
||||
1. **[Manage Users](../user-management/index.mdx)** - Update user profiles, manage user data
|
||||
2. **[Handle Teams](../team-management/index.mdx)** - Implement team functionality
|
||||
3. **[Set up OAuth](../oauth/index.mdx)** - Add social login providers
|
||||
4. **[Framework Integration](../integration/index.mdx)** - See examples for Flask, Django, and FastAPI
|
||||
|
||||
For more advanced authentication features, check out the [REST API documentation](../rest-api/overview.mdx).
|
||||
|
||||
|
||||
|
||||
|
||||
127
docs/templates-python/getting-started/setup.mdx
Normal file
127
docs/templates-python/getting-started/setup.mdx
Normal file
@ -0,0 +1,127 @@
|
||||
---
|
||||
title: Setup
|
||||
---
|
||||
|
||||
<Info>
|
||||
Welcome to the Python setup guide. If you're looking for guides for other frameworks, check out the [Next.js SDK Setup](/next/getting-started/setup), [React SDK Setup](/react/getting-started/setup), or the [JavaScript SDK Setup](/js/getting-started/setup).
|
||||
</Info>
|
||||
|
||||
Our recommended way to use Stack Auth with Python is with the [REST API](../rest-api/overview.mdx). It provides a fully documented way to interact with Stack Auth from any Python framework, including Flask, FastAPI, Django, and others.
|
||||
|
||||
For the purpose of this guide, we will use the `requests` library to make HTTP requests to the Stack Auth API. If you haven't already, you can install it in your environment with `pip install requests`.
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Create API keys
|
||||
</Step>
|
||||
|
||||
First, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), and copy your project ID, publishable client key, and secret server key into a safe place (eg. environment variables).
|
||||
|
||||
From there, you can access them in your Python code. You can then read them like this:
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
stack_project_id = os.getenv("STACK_PROJECT_ID")
|
||||
stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY")
|
||||
stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")
|
||||
```
|
||||
|
||||
<Step>
|
||||
### Create a Stack Auth client
|
||||
</Step>
|
||||
|
||||
Next, create a helper class to make requests to the Stack Auth API. This will handle authentication headers and error handling:
|
||||
|
||||
```python
|
||||
import requests
|
||||
import os
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
class StackAuthClient:
|
||||
def __init__(self):
|
||||
self.base_url = "https://api.stack-auth.com"
|
||||
self.project_id = os.getenv("STACK_PROJECT_ID")
|
||||
self.publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY")
|
||||
self.secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")
|
||||
|
||||
if not all([self.project_id, self.publishable_client_key]):
|
||||
raise ValueError("Missing required Stack Auth environment variables")
|
||||
|
||||
def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
access_type: str = "server",
|
||||
access_token: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Make a request to the Stack Auth API"""
|
||||
headers = {
|
||||
'x-stack-access-type': access_type,
|
||||
'x-stack-project-id': self.project_id,
|
||||
'x-stack-publishable-client-key': self.publishable_client_key,
|
||||
'Content-Type': 'application/json',
|
||||
**kwargs.pop('headers', {}),
|
||||
}
|
||||
|
||||
# Add server key for server access
|
||||
if access_type == "server" and self.secret_server_key:
|
||||
headers['x-stack-secret-server-key'] = self.secret_server_key
|
||||
|
||||
# Add access token for authenticated requests
|
||||
if access_token:
|
||||
headers['x-stack-access-token'] = access_token
|
||||
|
||||
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
||||
|
||||
response = requests.request(method, url, headers=headers, **kwargs)
|
||||
|
||||
if response.status_code >= 400:
|
||||
raise Exception(f"Stack Auth API request failed with {response.status_code}: {response.text}")
|
||||
|
||||
return response.json() if response.content else {}
|
||||
|
||||
# Initialize the client
|
||||
stack_auth = StackAuthClient()
|
||||
```
|
||||
|
||||
<Step>
|
||||
### Test the connection
|
||||
</Step>
|
||||
|
||||
Test that your API keys work correctly by fetching the current project:
|
||||
|
||||
```python
|
||||
# Test the connection
|
||||
try:
|
||||
project_info = stack_auth._make_request('GET', '/api/v1/projects/current')
|
||||
print("✅ Successfully connected to Stack Auth!")
|
||||
print(f"Project: {project_info.get('display_name', 'Unknown')}")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to connect: {e}")
|
||||
```
|
||||
|
||||
<Step>
|
||||
### Done!
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## What's Next?
|
||||
|
||||
Now that you have Stack Auth set up in your Python application, you can:
|
||||
|
||||
1. **[Implement Authentication](../authentication/index.mdx)** - Learn how to sign up and sign in users
|
||||
2. **[Manage Users](../user-management/index.mdx)** - Create, update, and retrieve user information
|
||||
3. **[Handle Teams](../team-management/index.mdx)** - Implement team functionality
|
||||
4. **[Framework Integration](../integration/index.mdx)** - See specific examples for Flask, Django, and FastAPI
|
||||
|
||||
## Framework-Specific Setup
|
||||
|
||||
While the REST API approach works with any Python framework, we also provide specific integration guides:
|
||||
|
||||
- **[Flask Integration](../integration/flask.mdx)** - Complete Flask setup with middleware
|
||||
- **[Django Integration](../integration/django.mdx)** - Django setup with custom authentication backend
|
||||
- **[FastAPI Integration](../integration/fastapi.mdx)** - FastAPI setup with dependency injection
|
||||
|
||||
Choose the guide that matches your framework, or continue with the general REST API approach if you're using a different framework.
|
||||
Loading…
Reference in New Issue
Block a user