this repo has no description

cleanup code

+3
deno.lock
··· 160 160 "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 161 161 } 162 162 }, 163 + "remote": { 164 + "https://esm.smallweb.run/slice@e3c3789/src/generated_client.ts": "d2b713a3ae4f4d9b130fe308f9228063fb950ce0f8921facd7fcbf3a6f6732b8" 165 + }, 163 166 "workspace": { 164 167 "dependencies": [ 165 168 "jsr:@slices/client@~0.1.0-alpha.4",
+3
main.ts
··· 1 + export default { 2 + fetch: () => Response.redirect("https://slices.network/profile/pomdtr.me/slice/3m25liqjjnh2o", 302), 3 + }
+1 -1
slices.json
··· 1 1 { 2 2 "slice": "at://did:plc:ovreo3dlfroo4ztkep3kjlle/network.slices.slice/3m25liqjjnh2o", 3 3 "lexiconPath": "./lexicons", 4 - "clientOutputPath": "./src/generated_client.ts" 4 + "clientOutputPath": "./client.ts" 5 5 }
+1 -1
smallweb.json
··· 1 1 { 2 - "entrypoint": "./src/main.ts" 2 + "atUri": "at://did:plc:o7uo6lvq73v3phueoo52eppa/run.smallweb.app/3m2c52ygb7m2k" 3 3 }
-75
src/config.ts
··· 1 - import { OAuthClient, SQLiteOAuthStorage } from "@slices/oauth"; 2 - import { SessionStore, SQLiteAdapter, withOAuthSession } from "@slices/session"; 3 - import { AtProtoClient } from "./generated_client.ts"; 4 - 5 - const OAUTH_CLIENT_ID = Deno.env.get("OAUTH_CLIENT_ID"); 6 - const OAUTH_CLIENT_SECRET = Deno.env.get("OAUTH_CLIENT_SECRET"); 7 - const OAUTH_REDIRECT_URI = Deno.env.get("OAUTH_REDIRECT_URI"); 8 - const OAUTH_AIP_BASE_URL = Deno.env.get("OAUTH_AIP_BASE_URL"); 9 - const API_URL = Deno.env.get("API_URL"); 10 - export const SLICE_URI = Deno.env.get("SLICE_URI"); 11 - 12 - if ( 13 - !OAUTH_CLIENT_ID || 14 - !OAUTH_CLIENT_SECRET || 15 - !OAUTH_REDIRECT_URI || 16 - !OAUTH_AIP_BASE_URL || 17 - !API_URL || 18 - !SLICE_URI 19 - ) { 20 - throw new Error( 21 - "Missing OAuth configuration. Please ensure .env file contains:\n" + 22 - "OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_AIP_BASE_URL, API_URL, SLICE_URI" 23 - ); 24 - } 25 - 26 - const DATABASE_URL = Deno.env.get("DATABASE_URL") || "slices.db"; 27 - 28 - // OAuth setup 29 - const oauthStorage = new SQLiteOAuthStorage(DATABASE_URL); 30 - const oauthConfig = { 31 - clientId: OAUTH_CLIENT_ID, 32 - clientSecret: OAUTH_CLIENT_SECRET, 33 - authBaseUrl: OAUTH_AIP_BASE_URL, 34 - redirectUri: OAUTH_REDIRECT_URI, 35 - scopes: ["atproto", "openid", "profile"], 36 - }; 37 - 38 - // Export config and storage for creating user-scoped clients 39 - export { oauthConfig, oauthStorage }; 40 - 41 - // Session setup (shared database) 42 - export const sessionStore = new SessionStore({ 43 - adapter: new SQLiteAdapter(DATABASE_URL), 44 - cookieName: "slice-session", 45 - cookieOptions: { 46 - httpOnly: true, 47 - secure: Deno.env.get("DENO_ENV") === "production", 48 - sameSite: "lax", 49 - path: "/", 50 - }, 51 - }); 52 - 53 - // OAuth + Session integration 54 - export const oauthSessions = withOAuthSession( 55 - sessionStore, 56 - oauthConfig, 57 - oauthStorage, 58 - { 59 - autoRefresh: true, 60 - } 61 - ); 62 - 63 - // Helper function to create user-scoped OAuth client 64 - export function createOAuthClient(userId: string): OAuthClient { 65 - return new OAuthClient(oauthConfig, oauthStorage, userId); 66 - } 67 - 68 - // Helper function to create authenticated AtProto client for a user 69 - export function createSessionClient(userId: string): AtProtoClient { 70 - const userOAuthClient = createOAuthClient(userId); 71 - return new AtProtoClient(API_URL!, SLICE_URI!, userOAuthClient); 72 - } 73 - 74 - // Public client for unauthenticated requests 75 - export const publicClient = new AtProtoClient(API_URL, SLICE_URI);
-169
src/features/auth/handlers.tsx
··· 1 - import type { Route } from "@std/http/unstable-route"; 2 - import { withAuth } from "../../routes/middleware.ts"; 3 - import { OAuthClient } from "@slices/oauth"; 4 - import { 5 - createOAuthClient, 6 - createSessionClient, 7 - oauthConfig, 8 - oauthStorage, 9 - oauthSessions, 10 - sessionStore, 11 - } from "../../config.ts"; 12 - import { renderHTML } from "../../utils/render.tsx"; 13 - import { LoginPage } from "./templates/LoginPage.tsx"; 14 - 15 - async function handleLoginPage(req: Request): Promise<Response> { 16 - const context = await withAuth(req); 17 - const url = new URL(req.url); 18 - 19 - // Redirect if already logged in 20 - if (context.currentUser) { 21 - return Response.redirect(new URL("/dashboard", req.url), 302); 22 - } 23 - 24 - const error = url.searchParams.get("error"); 25 - return renderHTML(<LoginPage error={error || undefined} />); 26 - } 27 - 28 - async function handleOAuthAuthorize(req: Request): Promise<Response> { 29 - try { 30 - const formData = await req.formData(); 31 - const loginHint = formData.get("loginHint") as string; 32 - 33 - if (!loginHint) { 34 - return new Response("Missing login hint", { status: 400 }); 35 - } 36 - 37 - const tempOAuthClient = new OAuthClient( 38 - oauthConfig, 39 - oauthStorage, 40 - loginHint 41 - ); 42 - const authResult = await tempOAuthClient.authorize({ 43 - loginHint, 44 - }); 45 - 46 - return Response.redirect(authResult.authorizationUrl, 302); 47 - } catch (error) { 48 - console.error("OAuth authorize error:", error); 49 - 50 - return Response.redirect( 51 - new URL( 52 - "/login?error=" + 53 - encodeURIComponent("Please check your handle and try again."), 54 - req.url 55 - ), 56 - 302 57 - ); 58 - } 59 - } 60 - 61 - async function handleOAuthCallback(req: Request): Promise<Response> { 62 - try { 63 - const url = new URL(req.url); 64 - const code = url.searchParams.get("code"); 65 - const state = url.searchParams.get("state"); 66 - 67 - if (!code || !state) { 68 - return Response.redirect( 69 - new URL( 70 - "/login?error=" + encodeURIComponent("Invalid OAuth callback"), 71 - req.url 72 - ), 73 - 302 74 - ); 75 - } 76 - 77 - const tempOAuthClient = new OAuthClient(oauthConfig, oauthStorage, "temp"); 78 - const tokens = await tempOAuthClient.handleCallback({ code, state }); 79 - const sessionId = await oauthSessions.createOAuthSession(tokens); 80 - 81 - if (!sessionId) { 82 - return Response.redirect( 83 - new URL( 84 - "/login?error=" + encodeURIComponent("Failed to create session"), 85 - req.url 86 - ), 87 - 302 88 - ); 89 - } 90 - 91 - const sessionCookie = sessionStore.createSessionCookie(sessionId); 92 - 93 - let userInfo; 94 - try { 95 - const sessionOAuthClient = createOAuthClient(sessionId); 96 - userInfo = await sessionOAuthClient.getUserInfo(); 97 - } catch (error) { 98 - console.error("Failed to get user info:", error); 99 - } 100 - 101 - if (userInfo?.sub) { 102 - try { 103 - const userClient = createSessionClient(sessionId); 104 - await userClient.syncUserCollections(); 105 - console.log("Synced Bluesky profile for", userInfo.sub); 106 - } catch (error) { 107 - console.error("Error syncing Bluesky profile:", error); 108 - } 109 - } 110 - 111 - return new Response(null, { 112 - status: 302, 113 - headers: { 114 - Location: new URL("/dashboard", req.url).toString(), 115 - "Set-Cookie": sessionCookie, 116 - }, 117 - }); 118 - } catch (error) { 119 - console.error("OAuth callback error:", error); 120 - return Response.redirect( 121 - new URL( 122 - "/login?error=" + encodeURIComponent("Authentication failed"), 123 - req.url 124 - ), 125 - 302 126 - ); 127 - } 128 - } 129 - 130 - async function handleLogout(req: Request): Promise<Response> { 131 - const session = await sessionStore.getSessionFromRequest(req); 132 - 133 - if (session) { 134 - await oauthSessions.logout(session.sessionId); 135 - } 136 - 137 - const clearCookie = sessionStore.createLogoutCookie(); 138 - 139 - return new Response(null, { 140 - status: 302, 141 - headers: { 142 - Location: new URL("/login", req.url).toString(), 143 - "Set-Cookie": clearCookie, 144 - }, 145 - }); 146 - } 147 - 148 - export const authRoutes: Route[] = [ 149 - { 150 - method: "GET", 151 - pattern: new URLPattern({ pathname: "/login" }), 152 - handler: handleLoginPage, 153 - }, 154 - { 155 - method: "POST", 156 - pattern: new URLPattern({ pathname: "/oauth/authorize" }), 157 - handler: handleOAuthAuthorize, 158 - }, 159 - { 160 - method: "GET", 161 - pattern: new URLPattern({ pathname: "/oauth/callback" }), 162 - handler: handleOAuthCallback, 163 - }, 164 - { 165 - method: "POST", 166 - pattern: new URLPattern({ pathname: "/logout" }), 167 - handler: handleLogout, 168 - }, 169 - ];
-56
src/features/auth/templates/LoginPage.tsx
··· 1 - import { Layout } from "../../../shared/fragments/Layout.tsx"; 2 - import { Button } from "../../../shared/fragments/Button.tsx"; 3 - import { Input } from "../../../shared/fragments/Input.tsx"; 4 - 5 - interface LoginPageProps { 6 - error?: string; 7 - } 8 - 9 - export function LoginPage({ error }: LoginPageProps) { 10 - return ( 11 - <Layout title="Login"> 12 - <div className="min-h-screen flex items-center justify-center bg-gray-50"> 13 - <div className="max-w-md w-full space-y-8"> 14 - <div> 15 - <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900"> 16 - Sign in to your account 17 - </h2> 18 - <p className="mt-2 text-center text-sm text-gray-600"> 19 - Use your AT Protocol handle or DID 20 - </p> 21 - </div> 22 - 23 - {error && ( 24 - <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded"> 25 - {error === "OAuth initialization failed" && "Failed to start authentication"} 26 - {error === "Invalid OAuth callback" && "Authentication callback failed"} 27 - {error === "Authentication failed" && "Authentication failed"} 28 - {error === "Failed to create session" && "Failed to create session"} 29 - {!["OAuth initialization failed", "Invalid OAuth callback", "Authentication failed", "Failed to create session"].includes(error) && error} 30 - </div> 31 - )} 32 - 33 - <form className="mt-8 space-y-6" action="/oauth/authorize" method="post"> 34 - <div> 35 - <label htmlFor="loginHint" className="block text-sm font-medium text-gray-700"> 36 - Handle or DID 37 - </label> 38 - <Input 39 - id="loginHint" 40 - name="loginHint" 41 - type="text" 42 - required 43 - placeholder="alice.bsky.social or did:plc:..." 44 - className="mt-1" 45 - /> 46 - </div> 47 - 48 - <Button type="submit" className="w-full"> 49 - Sign in 50 - </Button> 51 - </form> 52 - </div> 53 - </div> 54 - </Layout> 55 - ); 56 - }
-49
src/features/dashboard/handlers.tsx
··· 1 - import type { Route } from "@std/http/unstable-route"; 2 - import { withAuth } from "../../routes/middleware.ts"; 3 - import { renderHTML } from "../../utils/render.tsx"; 4 - import { DashboardPage } from "./templates/DashboardPage.tsx"; 5 - import { publicClient } from "../../config.ts"; 6 - import { recordBlobToCdnUrl } from "@slices/client"; 7 - import { AppBskyActorProfile } from "../../generated_client.ts"; 8 - 9 - async function handleDashboard(req: Request): Promise<Response> { 10 - const context = await withAuth(req); 11 - 12 - if (!context.currentUser) { 13 - return Response.redirect(new URL("/login", req.url), 302); 14 - } 15 - 16 - let profile: AppBskyActorProfile | undefined; 17 - let avatarUrl: string | undefined; 18 - try { 19 - const profileResult = await publicClient.app.bsky.actor.profile.getRecord({ 20 - uri: `at://${context.currentUser.sub}/app.bsky.actor.profile/self`, 21 - }); 22 - 23 - if (profileResult) { 24 - profile = profileResult.value; 25 - 26 - if (profile.avatar) { 27 - avatarUrl = recordBlobToCdnUrl(profileResult, profile.avatar, "avatar"); 28 - } 29 - } 30 - } catch (error) { 31 - console.error("Error fetching profile:", error); 32 - } 33 - 34 - return renderHTML( 35 - <DashboardPage 36 - currentUser={context.currentUser} 37 - profile={profile} 38 - avatarUrl={avatarUrl} 39 - /> 40 - ); 41 - } 42 - 43 - export const dashboardRoutes: Route[] = [ 44 - { 45 - method: "GET", 46 - pattern: new URLPattern({ pathname: "/dashboard" }), 47 - handler: handleDashboard, 48 - }, 49 - ];
-56
src/features/dashboard/templates/DashboardPage.tsx
··· 1 - import { Layout } from "../../../shared/fragments/Layout.tsx"; 2 - import { Button } from "../../../shared/fragments/Button.tsx"; 3 - import type { AppBskyActorProfile } from "../../../generated_client.ts"; 4 - 5 - interface DashboardPageProps { 6 - currentUser: { 7 - name?: string; 8 - sub: string; 9 - }; 10 - profile?: AppBskyActorProfile; 11 - avatarUrl?: string; 12 - } 13 - 14 - export function DashboardPage({ 15 - currentUser, 16 - profile, 17 - avatarUrl, 18 - }: DashboardPageProps) { 19 - return ( 20 - <Layout title="Dashboard"> 21 - <div className="min-h-screen bg-gray-50 p-8"> 22 - <div className="max-w-2xl mx-auto"> 23 - <div className="bg-white rounded-lg shadow p-6"> 24 - <div className="flex justify-between items-center mb-6"> 25 - <h1 className="text-2xl font-bold">Dashboard</h1> 26 - <form method="post" action="/logout"> 27 - <Button type="submit" variant="secondary"> 28 - Logout 29 - </Button> 30 - </form> 31 - </div> 32 - 33 - <div className="mb-6"> 34 - {avatarUrl && ( 35 - <img 36 - src={avatarUrl} 37 - alt="Profile" 38 - className="w-20 h-20 rounded-full mb-4" 39 - /> 40 - )} 41 - <h2 className="text-xl font-semibold mb-2"> 42 - {profile?.displayName || currentUser.name || currentUser.sub} 43 - </h2> 44 - {currentUser.name && ( 45 - <p className="text-gray-600 mb-2">@{currentUser.name}</p> 46 - )} 47 - {profile?.description && ( 48 - <p className="text-gray-700 mt-2">{profile.description}</p> 49 - )} 50 - </div> 51 - </div> 52 - </div> 53 - </div> 54 - </Layout> 55 - ); 56 - }
+1 -1
src/generated_client.ts client.ts
··· 1 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-10-02 13:48:07 UTC 2 + // Generated at: 2025-10-03 14:17:15 UTC 3 3 // Lexicons: 2 4 4 5 5 /**
-14
src/main.ts
··· 1 - import { route } from "@std/http/unstable-route"; 2 - import { allRoutes } from "./routes/mod.ts"; 3 - import { createLoggingHandler } from "./utils/logging.ts"; 4 - 5 - function defaultHandler(req: Request): Promise<Response> { 6 - return Promise.resolve(Response.redirect(new URL("/", req.url), 302)); 7 - } 8 - 9 - const handler = createLoggingHandler(route(allRoutes, defaultHandler)); 10 - 11 - export default { 12 - fetch: handler, 13 - } 14 -
-43
src/routes/middleware.ts
··· 1 - import { sessionStore, createOAuthClient } from "../config.ts"; 2 - 3 - export interface AuthContext { 4 - currentUser: { 5 - sub: string; 6 - name?: string; 7 - email?: string; 8 - } | null; 9 - sessionId: string | null; 10 - } 11 - 12 - export async function withAuth(req: Request): Promise<AuthContext> { 13 - const session = await sessionStore.getSessionFromRequest(req); 14 - 15 - if (!session) { 16 - return { currentUser: null, sessionId: null }; 17 - } 18 - 19 - try { 20 - const sessionOAuthClient = createOAuthClient(session.sessionId); 21 - const userInfo = await sessionOAuthClient.getUserInfo(); 22 - return { 23 - currentUser: userInfo || null, 24 - sessionId: session.sessionId, 25 - }; 26 - } catch { 27 - return { currentUser: null, sessionId: session.sessionId }; 28 - } 29 - } 30 - 31 - export function requireAuth( 32 - handler: (req: Request, context: AuthContext) => Promise<Response> 33 - ) { 34 - return async (req: Request): Promise<Response> => { 35 - const context = await withAuth(req); 36 - 37 - if (!context.currentUser) { 38 - return Response.redirect(new URL("/login", req.url), 302); 39 - } 40 - 41 - return handler(req, context); 42 - }; 43 - }
-24
src/routes/mod.ts
··· 1 - import type { Route } from "@std/http/unstable-route"; 2 - import { authRoutes } from "../features/auth/handlers.tsx"; 3 - import { dashboardRoutes } from "../features/dashboard/handlers.tsx"; 4 - 5 - export const allRoutes: Route[] = [ 6 - // Root redirect to login for now 7 - { 8 - method: "GET", 9 - pattern: new URLPattern({ pathname: "/" }), 10 - handler: (req) => Response.redirect(new URL("/login", req.url), 302), 11 - }, 12 - 13 - { 14 - method: "GET", 15 - pattern: new URLPattern({ pathname: "/admin" }), 16 - handler: () => Response.redirect("https://slices.network/profile/pomdtr.me/slice/3m25liqjjnh2o", 302), 17 - }, 18 - 19 - // Auth routes 20 - ...authRoutes, 21 - 22 - // Dashboard routes 23 - ...dashboardRoutes, 24 - ];
-50
src/shared/fragments/Button.tsx
··· 1 - import { ComponentChildren, JSX } from "preact"; 2 - import { cn } from "../../utils/cn.ts"; 3 - 4 - interface ButtonProps extends Omit<JSX.IntrinsicElements['button'], "size"> { 5 - children: ComponentChildren; 6 - variant?: "primary" | "secondary" | "danger"; 7 - size?: "sm" | "md" | "lg"; 8 - } 9 - 10 - export function Button({ 11 - children, 12 - type = "button", 13 - variant = "primary", 14 - size = "md", 15 - className, 16 - disabled, 17 - ...props 18 - }: ButtonProps) { 19 - const baseClasses = 20 - "inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; 21 - 22 - const variantClasses = { 23 - primary: "bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500", 24 - secondary: 25 - "bg-gray-200 hover:bg-gray-300 text-gray-900 focus:ring-gray-500", 26 - danger: "bg-red-600 hover:bg-red-700 text-white focus:ring-red-500", 27 - }; 28 - 29 - const sizeClasses = { 30 - sm: "px-3 py-1.5 text-sm", 31 - md: "px-4 py-2 text-sm", 32 - lg: "px-6 py-3 text-base", 33 - }; 34 - 35 - return ( 36 - <button 37 - type={type} 38 - disabled={disabled} 39 - className={cn( 40 - baseClasses, 41 - variantClasses[variant], 42 - sizeClasses[size], 43 - className 44 - )} 45 - {...props} 46 - > 47 - {children} 48 - </button> 49 - ); 50 - }
-23
src/shared/fragments/Input.tsx
··· 1 - import { JSX } from "preact"; 2 - import { cn } from "../../utils/cn.ts"; 3 - 4 - type InputProps = JSX.IntrinsicElements['input']; 5 - 6 - export function Input({ 7 - type = "text", 8 - className, 9 - ...props 10 - }: InputProps) { 11 - return ( 12 - <input 13 - type={type} 14 - className={cn( 15 - "block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm", 16 - "focus:outline-none focus:ring-blue-500 focus:border-blue-500", 17 - "disabled:bg-gray-50 disabled:text-gray-500", 18 - className 19 - )} 20 - {...props} 21 - /> 22 - ); 23 - }
-23
src/shared/fragments/Layout.tsx
··· 1 - import { ComponentChildren } from "preact"; 2 - 3 - interface LayoutProps { 4 - title?: string; 5 - children: ComponentChildren; 6 - } 7 - 8 - export function Layout({ title = "App", children }: LayoutProps) { 9 - return ( 10 - <html lang="en"> 11 - <head> 12 - <meta charset="UTF-8" /> 13 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 14 - <title>{title}</title> 15 - <script src="https://cdn.tailwindcss.com"></script> 16 - <script src="https://unpkg.com/htmx.org@1.9.10"></script> 17 - </head> 18 - <body> 19 - {children} 20 - </body> 21 - </html> 22 - ); 23 - }
-6
src/utils/cn.ts
··· 1 - import { type ClassValue, clsx } from "clsx"; 2 - import { twMerge } from "tailwind-merge"; 3 - 4 - export function cn(...inputs: ClassValue[]): string { 5 - return twMerge(clsx(inputs)); 6 - }
-43
src/utils/logging.ts
··· 1 - import { cyan, green, red, yellow, bold, dim } from "@std/fmt/colors"; 2 - 3 - export function createLoggingHandler( 4 - handler: (req: Request) => Response | Promise<Response> 5 - ) { 6 - return async (req: Request): Promise<Response> => { 7 - const start = Date.now(); 8 - const method = req.method; 9 - const url = new URL(req.url); 10 - 11 - try { 12 - const response = await Promise.resolve(handler(req)); 13 - const duration = Date.now() - start; 14 - 15 - const methodColor = cyan(bold(method)); 16 - const statusColor = 17 - response.status >= 200 && response.status < 300 18 - ? green(String(response.status)) 19 - : response.status >= 300 && response.status < 400 20 - ? yellow(String(response.status)) 21 - : response.status >= 400 22 - ? red(String(response.status)) 23 - : String(response.status); 24 - const durationText = dim(`(${duration}ms)`); 25 - 26 - console.log( 27 - `${methodColor} ${url.pathname} - ${statusColor} ${durationText}` 28 - ); 29 - return response; 30 - } catch (error) { 31 - const duration = Date.now() - start; 32 - const methodColor = cyan(bold(method)); 33 - const errorText = red(bold("ERROR")); 34 - const durationText = dim(`(${duration}ms)`); 35 - 36 - console.error( 37 - `${methodColor} ${url.pathname} - ${errorText} ${durationText}:`, 38 - error 39 - ); 40 - throw error; 41 - } 42 - }; 43 - }
-12
src/utils/render.tsx
··· 1 - import { renderToString } from "preact-render-to-string"; 2 - import { VNode } from "preact"; 3 - 4 - export function renderHTML(element: VNode): Response { 5 - const html = renderToString(element); 6 - 7 - return new Response(html, { 8 - headers: { 9 - "Content-Type": "text/html; charset=utf-8", 10 - }, 11 - }); 12 - }