41 |
Welcome Guest!
42 |
43 | This example demonstrates Corbado's passkey-first authentication
44 | solution.
45 |
46 |
It covers all relevant aspects like -
47 |
48 | Sign-up
49 | Login
50 | Protecting Routes
51 |
52 |
53 | It can be used as a starting point for your own application or
54 | to learn.
55 |
56 |
57 | Sign up
58 |
59 |
60 | Login
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/src/pages/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import { CorbadoAuth } from "@corbado/react";
2 | import { use, useEffect } from "react";
3 | import { UserContext } from "../context/user.tsx";
4 | import { useNavigate } from "react-router";
5 |
6 | export default function LoginPage() {
7 | const userCtx = use(UserContext);
8 | const navigate = useNavigate();
9 |
10 | useEffect(() => {
11 | const externalUserInfo = userCtx.externalUserInfo;
12 | switch (externalUserInfo.status) {
13 | case "success":
14 | if (externalUserInfo.user.city === null) {
15 | navigate("/signup/onboarding");
16 | } else {
17 | navigate("/profile");
18 | }
19 | return;
20 | case "error":
21 | // handle this case more gracefully in a real application
22 | console.error(externalUserInfo.message);
23 | return;
24 | }
25 | }, [navigate, userCtx.externalUserInfo]);
26 |
27 | return (
28 | ) {
16 | event.preventDefault();
17 | const formData = new FormData(event.currentTarget);
18 | const city = formData.get("city") as string;
19 | try {
20 | await userCtx.updateUserCity(city);
21 | } catch (e) {
22 | console.error("Failed to update user city:", e);
23 | }
24 | navigate("/");
25 | }
26 |
27 | return (
28 |
29 |
Onboarding
30 | Choose your city
31 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/pages/ProfilePage.tsx:
--------------------------------------------------------------------------------
1 | import { UserContext } from "../context/user.tsx";
2 | import { use } from "react";
3 | import { useNavigate } from "react-router";
4 | import { PasskeyList, useCorbado } from "@corbado/react";
5 |
6 | export default function ProfilePage() {
7 | const userCtx = use(UserContext);
8 | const navigate = useNavigate();
9 | const { isAuthenticated, loading } = useCorbado();
10 |
11 | if (!isAuthenticated && !loading) {
12 | navigate("/login");
13 | }
14 |
15 | const userInfo =
16 | userCtx.externalUserInfo.status === "success"
17 | ? userCtx.externalUserInfo
18 | : null;
19 |
20 | return (
21 |
22 |
Profile
23 |
24 | Example userID:
25 | {userInfo?.user.id}
26 |
27 |
28 | Corbado userID:
29 | {userInfo?.user.corbado_user_id}
30 |
31 |
Your Identifiers
32 | {userInfo && (
33 |
34 | {userInfo.identifiers.map((identifier) => (
35 |
36 |
37 | Type:
38 | {identifier.type}
39 |
40 |
41 | Value:
42 | {identifier.value}
43 |
44 |
45 | ))}
46 |
47 | )}
48 |
49 |
50 | );
51 | }
52 |
53 | function PasskeyManagement() {
54 | return (
55 | <>
56 | Manage your Passkeys
57 |
58 | >
59 | );
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/frontend/src/pages/SignupPage.tsx:
--------------------------------------------------------------------------------
1 | import { CorbadoAuth } from "@corbado/react";
2 | import { use, useEffect } from "react";
3 | import { UserContext } from "../context/user.tsx";
4 | import { useNavigate } from "react-router";
5 |
6 | export default function SignupPage() {
7 | const userCtx = use(UserContext);
8 | const navigate = useNavigate();
9 |
10 | useEffect(() => {
11 | const externalUserInfo = userCtx.externalUserInfo;
12 | switch (externalUserInfo.status) {
13 | case "success":
14 | if (externalUserInfo.user.city === null) {
15 | navigate("/signup/onboarding");
16 | } else {
17 | navigate("/profile");
18 | }
19 | return;
20 | case "error":
21 | // handle this case more gracefully in a real application
22 | console.error(externalUserInfo.message);
23 | return;
24 | }
25 | }, [navigate, userCtx.externalUserInfo]);
26 |
27 | return (
28 |
29 |
Signup
30 | {
32 | // do nothing here. We have to wait for a backend response
33 | // to check whether the user has gone through onboarding already.
34 | // The backend call is made in the user store.
35 | }}
36 | initialBlock="signup-init"
37 | />
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/frontend/src/pages/UserAreaPage.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router";
2 | import { useCorbado } from "@corbado/react";
3 | import { useQuery } from "@tanstack/react-query";
4 |
5 | export default function UserAreaPage() {
6 | const { isAuthenticated, loading } = useCorbado();
7 |
8 | return isAuthenticated && !loading ? (
9 |
10 | ) : (
11 |
12 | );
13 | }
14 |
15 | function UserAreaAuthenticated() {
16 | const { sessionToken, loading } = useCorbado();
17 | const {
18 | data: secret,
19 | isLoading,
20 | error,
21 | refetch,
22 | } = useQuery({
23 | queryKey: ["get-secret"],
24 | queryFn: async () => {
25 | const res = await fetch(
26 | `${import.meta.env.VITE_BACKEND_BASE_URL}/api/secret`,
27 | {
28 | headers: {
29 | Authorization: `Bearer ${sessionToken}`,
30 | },
31 | },
32 | );
33 | return (await res.json()).secret as string;
34 | },
35 | enabled: false,
36 | });
37 |
38 | return (
39 |
40 |
User area!
41 |
Since you are logged-in, we can tell you a secret:
42 |
refetch()}
45 | disabled={loading || isLoading || !!secret}
46 | >
47 | Reveal secret
48 |
49 |
50 |
55 |
56 |
57 | );
58 | }
59 |
60 | function RevealSecretResult({
61 | secret,
62 | loading,
63 | error,
64 | }: {
65 | secret: string | undefined;
66 | loading: boolean;
67 | error: Error | null;
68 | }) {
69 | if (secret) {
70 | return (
71 |
72 |
Secret:
73 |
{secret}
74 |
75 | );
76 | }
77 | if (loading) {
78 | return
;
79 | }
80 | if (error) {
81 | return (
82 |
83 |
Failed to reveal secret: {error?.message}
84 |
85 | );
86 | }
87 | return null;
88 | }
89 |
90 | function UserAreaGuest() {
91 | return (
92 |
93 |
User area!
94 |
This page is for logged-in users only. Please login:
95 |
96 | Login
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/frontend/src/utils/corbado-translations.ts:
--------------------------------------------------------------------------------
1 | const englishTranslations = {
2 | signup: {
3 | "signup-init": {
4 | "signup-init": {
5 | header: "Let's create an account",
6 | subheader: "to check ",
7 | text_login: "Would you like to login? ",
8 | button_submit: "Sign up",
9 | textField_fullName: "Full Name",
10 | text_divider: "or use social logins",
11 | },
12 | },
13 | },
14 | login: {
15 | "login-init": {
16 | "login-init": {
17 | header: "Please login",
18 | subheader: "to check ",
19 | text_signup: "Would you like to create an account? ",
20 | button_signup: "Sign up",
21 | button_submit: "Login",
22 | },
23 | },
24 | },
25 | passkeysList: {
26 | button_createPasskey: "You can create passkeys here.",
27 | field_credentialId: "ID: ",
28 | field_status: "Status of Passkey: ",
29 | },
30 | };
31 |
32 | export default englishTranslations;
33 |
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": [
7 | "ES2020",
8 | "DOM",
9 | "DOM.Iterable"
10 | ],
11 | "module": "ESNext",
12 | "skipLibCheck": true,
13 | /* Bundler mode */
14 | "moduleResolution": "bundler",
15 | "allowImportingTsExtensions": true,
16 | "isolatedModules": true,
17 | "moduleDetection": "force",
18 | "noEmit": true,
19 | "jsx": "react-jsx",
20 | /* Linting */
21 | "strict": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true,
25 | "noUncheckedSideEffectImports": true
26 | },
27 | "include": [
28 | "src"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": [
6 | "ES2023"
7 | ],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": [
24 | "vite.config.ts"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react-swc";
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | });
8 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Exit immediately if a command exits with a non-zero status
4 | set -e
5 |
6 | # Function to handle termination
7 | cleanup() {
8 | echo -e "\nStopping all processes..."
9 | kill "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null
10 | exit 0
11 | }
12 |
13 | # Trap SIGINT and SIGTERM to run the cleanup function
14 | trap cleanup SIGINT SIGTERM
15 |
16 | # Start the backend
17 | (cd backend && npm run dev) | while IFS= read -r line; do
18 | echo -e "[BACKEND] $line"
19 | done &
20 | BACKEND_PID=$!
21 |
22 | # Start the frontend
23 | (cd frontend && npm run dev) | while IFS= read -r line; do
24 | echo -e "[FRONTEND] $line"
25 | done &
26 | FRONTEND_PID=$!
27 |
28 | # Wait for both processes to finish
29 | wait
--------------------------------------------------------------------------------