Add example project
This commit is contained in:
parent
20b12f3c60
commit
db9e65e9d7
18 changed files with 4650 additions and 0 deletions
15
example/.env.example
Normal file
15
example/.env.example
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Generate one with:
|
||||||
|
# pnpm --filter @tauth-example/backend exec cargo run --bin generate_keys
|
||||||
|
TAUTH_PRIVATE_KEY=
|
||||||
|
|
||||||
|
# Your TAuth app/domain identifier.
|
||||||
|
TAUTH_IDENTIFIER=example.local
|
||||||
|
|
||||||
|
# Must be reachable by TAuth. For local testing, expose the backend with a tunnel.
|
||||||
|
TAUTH_REDIRECT_URL=http://localhost:8787/tauth/callback
|
||||||
|
|
||||||
|
# Optional. If Omikron does not return a public key, set it here to enable MTP app-data sync.
|
||||||
|
TAUTH_HOST_PUBLIC_KEY=
|
||||||
|
|
||||||
|
BACKEND_ADDR=127.0.0.1:8787
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
5
example/.gitignore
vendored
Normal file
5
example/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
target/
|
||||||
|
backend/data/db.json
|
||||||
|
backend/.env
|
||||||
52
example/README.md
Normal file
52
example/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# TAuth SDK Vite + React Example
|
||||||
|
|
||||||
|
This example has a Vite/React frontend and a tiny Rust backend that uses the local `tauth-sdk` crate.
|
||||||
|
|
||||||
|
It demonstrates:
|
||||||
|
|
||||||
|
- creating a TAuth login URL with `TAuthClient::auth_url`
|
||||||
|
- parsing the TAuth callback with `TAuthClient::parse_callback`
|
||||||
|
- fetching the logged-in user's TAuth profile
|
||||||
|
- storing the session/user/app data in `backend/data/db.json`
|
||||||
|
- optionally syncing app data through MTP when `TAUTH_HOST_PUBLIC_KEY` is set
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Install frontend dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the environment template:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example backend/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `TAUTH_PRIVATE_KEY` in `backend/.env`. Generate keys with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @tauth-example/backend exec cargo run --bin generate_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
For local callback testing, `TAUTH_REDIRECT_URL` must be reachable by TAuth. Use a tunnel if needed.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
Run frontend and backend together:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:5173` and click `Login with TAuth`.
|
||||||
|
|
||||||
|
## Backend Endpoints
|
||||||
|
|
||||||
|
- `GET /api/login`: redirects to TAuth
|
||||||
|
- `GET /tauth/callback`: receives TAuth callback and stores the session
|
||||||
|
- `GET /api/me`: returns the current cookie session and cached user data
|
||||||
|
- `PUT /api/app-data`: saves JSON app data locally and optionally through MTP
|
||||||
|
|
||||||
|
The JSON database is intentionally simple for example purposes. Do not use it as-is for production.
|
||||||
2758
example/backend/Cargo.lock
generated
Normal file
2758
example/backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
example/backend/Cargo.toml
Normal file
14
example/backend/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "tauth-example-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.8"
|
||||||
|
dotenvy = "0.15"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tauth-sdk = { path = "../.." }
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] }
|
||||||
|
tower-http = { version = "0.6", features = ["cors"] }
|
||||||
|
url = "2"
|
||||||
9
example/backend/package.json
Normal file
9
example/backend/package.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@tauth-example/backend",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cargo run",
|
||||||
|
"build": "cargo build --release",
|
||||||
|
"test": "cargo test"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
example/backend/src/bin/generate_keys.rs
Normal file
6
example/backend/src/bin/generate_keys.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
fn main() {
|
||||||
|
let keys = tauth_sdk::generate_key_pair();
|
||||||
|
|
||||||
|
println!("TAUTH_PRIVATE_KEY={}", keys.private);
|
||||||
|
println!("TAUTH_PUBLIC_KEY={}", keys.public);
|
||||||
|
}
|
||||||
255
example/backend/src/main.rs
Normal file
255
example/backend/src/main.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
use axum::extract::{Json, State};
|
||||||
|
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||||
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
|
use axum::routing::{get, put};
|
||||||
|
use axum::{Router, serve};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::env;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauth_sdk::{AuthCallback, TAuthClient, TAuthConfig, User};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
|
||||||
|
const SESSION_COOKIE: &str = "tauth_example_session";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
tauth: Arc<TAuthClient>,
|
||||||
|
db: Arc<Mutex<Db>>,
|
||||||
|
db_path: PathBuf,
|
||||||
|
frontend_url: String,
|
||||||
|
host_public_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
struct Db {
|
||||||
|
sessions: BTreeMap<String, StoredSession>,
|
||||||
|
app_data: BTreeMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct StoredSession {
|
||||||
|
user_id: u64,
|
||||||
|
session_id: u64,
|
||||||
|
user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct MeResponse {
|
||||||
|
logged_in: bool,
|
||||||
|
session_id: Option<u64>,
|
||||||
|
user: Option<User>,
|
||||||
|
app_data: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let _ = dotenvy::dotenv();
|
||||||
|
|
||||||
|
let state = match AppState::from_env().await {
|
||||||
|
Ok(state) => state,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("failed to start example backend: {error}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let addr: SocketAddr = env::var("BACKEND_ADDR")
|
||||||
|
.unwrap_or_else(|_| "127.0.0.1:8787".to_string())
|
||||||
|
.parse()
|
||||||
|
.expect("BACKEND_ADDR must be host:port");
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/api/login", get(login))
|
||||||
|
.route("/api/me", get(me))
|
||||||
|
.route("/api/app-data", put(save_app_data))
|
||||||
|
.route("/tauth/callback", get(callback))
|
||||||
|
.layer(CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(addr).await.expect("bind backend address");
|
||||||
|
println!("example backend listening on http://{addr}");
|
||||||
|
serve(listener, app).await.expect("serve backend");
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
async fn from_env() -> Result<Self, String> {
|
||||||
|
let private_key = env::var("TAUTH_PRIVATE_KEY")
|
||||||
|
.map_err(|_| "TAUTH_PRIVATE_KEY must be set".to_string())?;
|
||||||
|
let identifier = env::var("TAUTH_IDENTIFIER").unwrap_or_else(|_| "example.local".to_string());
|
||||||
|
let redirect_url = env::var("TAUTH_REDIRECT_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:8787/tauth/callback".to_string());
|
||||||
|
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "http://localhost:5173".to_string());
|
||||||
|
let host_public_key = env::var("TAUTH_HOST_PUBLIC_KEY").ok().filter(|value| !value.is_empty());
|
||||||
|
|
||||||
|
let tauth = TAuthClient::from_config(TAuthConfig::new(identifier, redirect_url, private_key))
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let db_path = PathBuf::from("data/db.json");
|
||||||
|
let db = load_db(&db_path).await.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tauth: Arc::new(tauth),
|
||||||
|
db: Arc::new(Mutex::new(db)),
|
||||||
|
db_path,
|
||||||
|
frontend_url,
|
||||||
|
host_public_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login(State(state): State<AppState>) -> Redirect {
|
||||||
|
Redirect::temporary(&state.tauth.auth_url(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn callback(State(state): State<AppState>, uri: axum::http::Uri) -> Result<Response, AppError> {
|
||||||
|
let callback_url = format!("{}{}", state.tauth.redirect_url().origin().ascii_serialization(), uri);
|
||||||
|
let callback = state.tauth.parse_callback(callback_url)?;
|
||||||
|
let user = state.tauth.user(callback.user_id).await?;
|
||||||
|
persist_session(&state, &callback, user).await?;
|
||||||
|
|
||||||
|
let mut response = Redirect::temporary(&state.frontend_url).into_response();
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::SET_COOKIE,
|
||||||
|
HeaderValue::from_str(&format!(
|
||||||
|
"{SESSION_COOKIE}={}; Path=/; HttpOnly; SameSite=Lax",
|
||||||
|
callback.session_id
|
||||||
|
))
|
||||||
|
.map_err(|error| AppError::Internal(error.to_string()))?,
|
||||||
|
);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn me(State(state): State<AppState>, headers: HeaderMap) -> Result<Json<MeResponse>, AppError> {
|
||||||
|
let Some(session_id) = read_session_cookie(&headers) else {
|
||||||
|
return Ok(Json(MeResponse {
|
||||||
|
logged_in: false,
|
||||||
|
session_id: None,
|
||||||
|
user: None,
|
||||||
|
app_data: None,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
let session_key = session_id.to_string();
|
||||||
|
let Some(session) = db.sessions.get(&session_key) else {
|
||||||
|
return Ok(Json(MeResponse {
|
||||||
|
logged_in: false,
|
||||||
|
session_id: None,
|
||||||
|
user: None,
|
||||||
|
app_data: None,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(MeResponse {
|
||||||
|
logged_in: true,
|
||||||
|
session_id: Some(session.session_id),
|
||||||
|
user: Some(session.user.clone()),
|
||||||
|
app_data: db.app_data.get(&session_key).cloned(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_app_data(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(data): Json<Value>,
|
||||||
|
) -> Result<Json<Value>, AppError> {
|
||||||
|
let session_id = read_session_cookie(&headers).ok_or(AppError::Unauthorized)?;
|
||||||
|
let session = {
|
||||||
|
let db = state.db.lock().await;
|
||||||
|
db.sessions
|
||||||
|
.get(&session_id.to_string())
|
||||||
|
.cloned()
|
||||||
|
.ok_or(AppError::Unauthorized)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(host_public_key) = &state.host_public_key {
|
||||||
|
let tauth_session = state
|
||||||
|
.tauth
|
||||||
|
.connect_session(session.user_id, session.session_id, host_public_key)
|
||||||
|
.await?;
|
||||||
|
tauth_session.save_json(&data).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut db = state.db.lock().await;
|
||||||
|
db.app_data.insert(session_id.to_string(), data.clone());
|
||||||
|
save_db(&state.db_path, &db).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn persist_session(state: &AppState, callback: &AuthCallback, user: User) -> Result<(), AppError> {
|
||||||
|
let mut db = state.db.lock().await;
|
||||||
|
db.sessions.insert(
|
||||||
|
callback.session_id.to_string(),
|
||||||
|
StoredSession {
|
||||||
|
user_id: callback.user_id,
|
||||||
|
session_id: callback.session_id,
|
||||||
|
user,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
save_db(&state.db_path, &db).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_session_cookie(headers: &HeaderMap) -> Option<u64> {
|
||||||
|
let cookie = headers.get(header::COOKIE)?.to_str().ok()?;
|
||||||
|
cookie.split(';').find_map(|part| {
|
||||||
|
let (name, value) = part.trim().split_once('=')?;
|
||||||
|
(name == SESSION_COOKIE).then(|| value.parse().ok()).flatten()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_db(path: &PathBuf) -> std::io::Result<Db> {
|
||||||
|
match tokio::fs::read_to_string(path).await {
|
||||||
|
Ok(raw) => Ok(serde_json::from_str(&raw).unwrap_or_default()),
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Db::default()),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_db(path: &PathBuf, db: &Db) -> Result<(), AppError> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
tokio::fs::create_dir_all(parent).await?;
|
||||||
|
}
|
||||||
|
let raw = serde_json::to_string_pretty(db).map_err(|error| AppError::Internal(error.to_string()))?;
|
||||||
|
tokio::fs::write(path, raw).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppError {
|
||||||
|
Unauthorized,
|
||||||
|
TAuth(tauth_sdk::TAuthError),
|
||||||
|
Io(std::io::Error),
|
||||||
|
Internal(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<tauth_sdk::TAuthError> for AppError {
|
||||||
|
fn from(error: tauth_sdk::TAuthError) -> Self {
|
||||||
|
Self::TAuth(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for AppError {
|
||||||
|
fn from(error: std::io::Error) -> Self {
|
||||||
|
Self::Io(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, message) = match self {
|
||||||
|
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "not logged in".to_string()),
|
||||||
|
AppError::TAuth(error) => (StatusCode::BAD_GATEWAY, error.to_string()),
|
||||||
|
AppError::Io(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()),
|
||||||
|
AppError::Internal(error) => (StatusCode::INTERNAL_SERVER_ERROR, error),
|
||||||
|
};
|
||||||
|
|
||||||
|
(status, message).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
13
example/frontend/index.html
Normal file
13
example/frontend/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="color-scheme" content="dark" />
|
||||||
|
<title>TAuth SDK Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
example/frontend/package.json
Normal file
21
example/frontend/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "@tauth-example/frontend",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 127.0.0.1 --port 5173",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview --host 127.0.0.1 --port 5173"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"vite": "^7.3.0",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-dom": "^19.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
139
example/frontend/src/main.tsx
Normal file
139
example/frontend/src/main.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { StrictMode, useEffect, useState } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
status: string;
|
||||||
|
username: string;
|
||||||
|
public_key: string;
|
||||||
|
user_id: number;
|
||||||
|
iota_id: number;
|
||||||
|
sub_level: number;
|
||||||
|
sub_end: number;
|
||||||
|
display: string;
|
||||||
|
status_message: string;
|
||||||
|
about: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MeResponse = {
|
||||||
|
logged_in: boolean;
|
||||||
|
session_id?: number;
|
||||||
|
user?: User;
|
||||||
|
app_data?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [me, setMe] = useState<MeResponse | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/me", { credentials: "include" });
|
||||||
|
if (!response.ok) throw new Error(await response.text());
|
||||||
|
setMe(await response.json());
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveExampleData() {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/app-data", {
|
||||||
|
method: "PUT",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
savedFrom: "vite-react-example",
|
||||||
|
savedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(await response.text());
|
||||||
|
await refresh();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refresh();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="shell">
|
||||||
|
<section className="hero">
|
||||||
|
<p className="eyebrow">TAuth SDK Example</p>
|
||||||
|
<h1>Minimal TAuth login with a Rust backend.</h1>
|
||||||
|
<p className="lede">
|
||||||
|
This Vite app calls a tiny backend that uses the local Rust TAuth SDK,
|
||||||
|
stores the callback session in JSON, and shows the logged-in user.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="actions">
|
||||||
|
<a className="primary" href="/api/login">
|
||||||
|
Login with TAuth
|
||||||
|
</a>
|
||||||
|
<button type="button" onClick={() => void refresh()}>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{error ? <pre className="error">{error}</pre> : null}
|
||||||
|
|
||||||
|
<section className="card">
|
||||||
|
<div className="cardHeader">
|
||||||
|
<h2>Session</h2>
|
||||||
|
<span className={me?.logged_in ? "pill good" : "pill"}>
|
||||||
|
{me?.logged_in ? "Logged in" : "Anonymous"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!me ? (
|
||||||
|
<p>Loading...</p>
|
||||||
|
) : me.logged_in && me.user ? (
|
||||||
|
<div className="grid">
|
||||||
|
<Info label="Display" value={me.user.display || me.user.username} />
|
||||||
|
<Info label="Username" value={me.user.username} />
|
||||||
|
<Info label="User ID" value={String(me.user.user_id)} />
|
||||||
|
<Info label="Session ID" value={String(me.session_id)} />
|
||||||
|
<Info label="Status" value={me.user.status_message || me.user.status} />
|
||||||
|
<Info label="Subscription" value={String(me.user.sub_level)} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p>No TAuth session yet. Click the login button to start the flow.</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="card">
|
||||||
|
<div className="cardHeader">
|
||||||
|
<h2>App Data</h2>
|
||||||
|
<button disabled={!me?.logged_in || saving} onClick={() => void saveExampleData()}>
|
||||||
|
{saving ? "Saving..." : "Save Example Data"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="json">{JSON.stringify(me?.app_data ?? null, null, 2)}</pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Info({ label, value }: { label: string; value?: string }) {
|
||||||
|
return (
|
||||||
|
<div className="info">
|
||||||
|
<span>{label}</span>
|
||||||
|
<strong>{value || "-"}</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
167
example/frontend/src/styles.css
Normal file
167
example/frontend/src/styles.css
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
font-family:
|
||||||
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
background: #07070a;
|
||||||
|
color: #f4f4f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(95, 91, 255, 0.35), transparent 34rem),
|
||||||
|
radial-gradient(circle at bottom right, rgba(24, 197, 141, 0.18), transparent 28rem),
|
||||||
|
#07070a;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
a.primary {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #f4f4f5;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.primary {
|
||||||
|
border: 0;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #14b8a6);
|
||||||
|
color: white;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
width: min(980px, calc(100% - 2rem));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding: 2rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
color: #5eead4;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(2.4rem, 7vw, 5.8rem);
|
||||||
|
line-height: 0.95;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lede {
|
||||||
|
max-width: 700px;
|
||||||
|
color: #c4c4cc;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions,
|
||||||
|
.cardHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHeader {
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 28px;
|
||||||
|
background: rgba(10, 10, 14, 0.72);
|
||||||
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.32);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info span {
|
||||||
|
display: block;
|
||||||
|
color: #a1a1aa;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info strong {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #d4d4d8;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill.good {
|
||||||
|
background: rgba(20, 184, 166, 0.18);
|
||||||
|
color: #5eead4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json,
|
||||||
|
.error {
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.shell {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
example/frontend/src/vite-env.d.ts
vendored
Normal file
1
example/frontend/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
20
example/frontend/tsconfig.json
Normal file
20
example/frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
12
example/frontend/vite.config.ts
Normal file
12
example/frontend/vite.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": "http://127.0.0.1:8787",
|
||||||
|
"/tauth": "http://127.0.0.1:8787",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
13
example/package.json
Normal file
13
example/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "tauth-sdk-example",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@11.8.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "pnpm --parallel --filter @tauth-example/frontend --filter @tauth-example/backend dev",
|
||||||
|
"build": "pnpm --filter @tauth-example/frontend build && pnpm --filter @tauth-example/backend build",
|
||||||
|
"test": "pnpm --filter @tauth-example/backend test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
1142
example/pnpm-lock.yaml
generated
Normal file
1142
example/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
8
example/pnpm-workspace.yaml
Normal file
8
example/pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
packages:
|
||||||
|
- "frontend"
|
||||||
|
- "backend"
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
Loading…
Reference in a new issue