Ask questions, share opinions, vote together
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

init

zeudev f62cefc7

+249
+2
.env.example
··· 1 + BASE_URL=http://127.0.0.1:8000 2 + COOKIE_SECRET=<`openssl rand -base64 64`>
+1
.gitignore
··· 1 + .env
+26
README.md
··· 1 + # Pedro 2 + ### Ask questions, share opinions, vote together 3 + 4 + ## Features Roadmap (WIP) 5 + - [ ] Questions and Answers 6 + - [ ] Polling 7 + - [ ] Consensus Opinions 8 + 9 + ## Getting Started 10 + 1. Set up `.env` file with a `COOKIE_SECRET` variable 11 + ``` 12 + COOKIE_SECRET=[run `openssl rand -base64 64` in the terminal] 13 + ``` 14 + 15 + 2. Run the web application 16 + ``` 17 + deno task start 18 + ``` 19 + 20 + ## Packages 21 + - [`Deno`](https://deno.com) - Runtime and package manager 22 + - [`Hono`](https://hono.dev) - Web application framework 23 + - [`HTMX`](https://htmx.org) - AJAX library 24 + - [`@tijs/atproto-oauth`](https://jsr.io/@tijs/atproto-oauth) - AT Protocol OAuth library 25 + 26 + _Source hosted on [Tangled](https://tangled.org/zeu.dev/atproto-oauth-deno), mirrored on [GitHub](https://github.com/zeucapua/atproto-oauth-deno-demo)_
+42
components.tsx
··· 1 + import { Context } from "hono"; 2 + import { oauth } from "./main.tsx"; 3 + import { FC, PropsWithChildren } from "hono/jsx"; 4 + 5 + export const SiteLayout: FC<PropsWithChildren<{ context: Context }>> = async ({ context, children }) => { 6 + const { session } = await oauth.getSessionFromRequest(context.req.raw); 7 + 8 + return ( 9 + <html> 10 + <head> 11 + <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script> 12 + </head> 13 + <body> 14 + <nav style={{ display: "flex", "flex-direction": "row", gap: "8px" }}> 15 + { session ? <LogoutForm /> : <LoginForm /> } 16 + <a href="/">README</a> 17 + <a href="/profile">Profile</a> 18 + </nav> 19 + <hr /> 20 + {children} 21 + <div id="error"></div> 22 + </body> 23 + </html> 24 + ); 25 + } 26 + 27 + export const LoginForm: FC = () => { 28 + return ( 29 + <form id="authForm" hx-swap-oob="true" method="get" action="/login"> 30 + <input name="handle" type="text" /> 31 + <button type="submit">Login</button> 32 + </form> 33 + ) 34 + } 35 + 36 + export const LogoutForm: FC = () => { 37 + return ( 38 + <form id="authForm" hx-post="/api/auth/logout" hx-swap="none"> 39 + <button type="submit">Logout</button> 40 + </form> 41 + ) 42 + }
+14
deno.json
··· 1 + { 2 + "imports": { 3 + "@tijs/atproto-oauth": "jsr:@tijs/atproto-oauth@^2.9.0", 4 + "@tijs/atproto-storage": "jsr:@tijs/atproto-storage@^1.0.0", 5 + "hono": "jsr:@hono/hono@^4.11.9" 6 + }, 7 + "tasks": { 8 + "start": "deno run --allow-net --allow-env --env-file --watch-hmr main.tsx" 9 + }, 10 + "compilerOptions": { 11 + "jsx": "precompile", 12 + "jsxImportSource": "hono/jsx" 13 + } 14 + }
+101
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "jsr:@hono/hono@^4.11.9": "4.11.9", 5 + "jsr:@panva/jose@6.1.0": "6.1.0", 6 + "jsr:@tijs/atproto-oauth@*": "2.4.0", 7 + "jsr:@tijs/atproto-oauth@^2.9.0": "2.9.0", 8 + "jsr:@tijs/atproto-sessions@2.1.0": "2.1.0", 9 + "jsr:@tijs/atproto-storage@*": "1.0.0", 10 + "jsr:@tijs/atproto-storage@0.1.1": "0.1.1", 11 + "jsr:@tijs/atproto-storage@1": "1.0.0", 12 + "jsr:@tijs/oauth-client-deno@4.0.2": "4.0.2", 13 + "jsr:@tijs/oauth-client-deno@5.1.0": "5.1.0", 14 + "npm:@atproto/syntax@0.3.0": "0.3.0", 15 + "npm:@atproto/syntax@0.4.0": "0.4.0", 16 + "npm:iron-session@8.0.4": "8.0.4" 17 + }, 18 + "jsr": { 19 + "@hono/hono@4.11.9": { 20 + "integrity": "c82c6b846abc3c1879d921d8365287d77cdef8073019f509ff80bf53033bdcba" 21 + }, 22 + "@panva/jose@6.1.0": { 23 + "integrity": "9ecffef33d822f4326341ace652bf30eef30d4dc9f7134faf7901e5480c2e761" 24 + }, 25 + "@tijs/atproto-oauth@2.4.0": { 26 + "integrity": "1e38182d3a9cde5c767429a09701cd9012dfc8ab7548d944b839c6c5e785ab84", 27 + "dependencies": [ 28 + "jsr:@tijs/atproto-sessions", 29 + "jsr:@tijs/oauth-client-deno@4.0.2", 30 + "npm:@atproto/syntax@0.3.0" 31 + ] 32 + }, 33 + "@tijs/atproto-oauth@2.9.0": { 34 + "integrity": "89f71b7c99960d595df38defedace9cbc0c56350a4771a4b2b7fe6cd505248a2", 35 + "dependencies": [ 36 + "jsr:@tijs/atproto-sessions", 37 + "jsr:@tijs/atproto-storage@0.1.1", 38 + "jsr:@tijs/oauth-client-deno@5.1.0", 39 + "npm:@atproto/syntax@0.3.0" 40 + ] 41 + }, 42 + "@tijs/atproto-sessions@2.1.0": { 43 + "integrity": "5b8779ca7af76e3825c515d7483bbda6982b0ab656e3670566da6eb9d39aef59", 44 + "dependencies": [ 45 + "npm:iron-session" 46 + ] 47 + }, 48 + "@tijs/atproto-storage@0.1.1": { 49 + "integrity": "384643401a7d15915a6fc86b273ba142a1825d5d46b692b0da3405e9f938f8ab" 50 + }, 51 + "@tijs/atproto-storage@1.0.0": { 52 + "integrity": "a7f03f14ad4846fb9df19c90ac3cb007cfdc8a40df0e1b33314feaeaad439d2e" 53 + }, 54 + "@tijs/oauth-client-deno@4.0.2": { 55 + "integrity": "759d7fa655642a87ccf46afddce84065ca3a49b4c0d387ae1f0b09b66a9f1d26", 56 + "dependencies": [ 57 + "jsr:@panva/jose", 58 + "npm:@atproto/syntax@0.4.0" 59 + ] 60 + }, 61 + "@tijs/oauth-client-deno@5.1.0": { 62 + "integrity": "ab8b0a6a13030f4730452557dadb9d5b822e44924efcadaf4579befa1883cfa9", 63 + "dependencies": [ 64 + "jsr:@panva/jose", 65 + "npm:@atproto/syntax@0.4.0" 66 + ] 67 + } 68 + }, 69 + "npm": { 70 + "@atproto/syntax@0.3.0": { 71 + "integrity": "sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==" 72 + }, 73 + "@atproto/syntax@0.4.0": { 74 + "integrity": "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==" 75 + }, 76 + "cookie@0.7.2": { 77 + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" 78 + }, 79 + "iron-session@8.0.4": { 80 + "integrity": "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==", 81 + "dependencies": [ 82 + "cookie", 83 + "iron-webcrypto", 84 + "uncrypto" 85 + ] 86 + }, 87 + "iron-webcrypto@1.2.1": { 88 + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==" 89 + }, 90 + "uncrypto@0.1.3": { 91 + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" 92 + } 93 + }, 94 + "workspace": { 95 + "dependencies": [ 96 + "jsr:@hono/hono@^4.11.9", 97 + "jsr:@tijs/atproto-oauth@^2.9.0", 98 + "jsr:@tijs/atproto-storage@1" 99 + ] 100 + } 101 + }
+63
main.tsx
··· 1 + import { Hono } from "hono"; 2 + import { createATProtoOAuth } from "@tijs/atproto-oauth"; 3 + import { MemoryStorage } from "@tijs/atproto-storage"; 4 + import { LoginForm, SiteLayout } from "./components.tsx"; 5 + 6 + const app = new Hono(); 7 + 8 + export const oauth = createATProtoOAuth({ 9 + baseUrl: Deno.env.get("BASE_URL")!, 10 + appName: "Pedro", 11 + cookieSecret: Deno.env.get("COOKIE_SECRET")!, 12 + storage: new MemoryStorage(), 13 + sessionTtl: 60 * 60 * 24 * 14, // 14 days 14 + }); 15 + 16 + 17 + app.get("/login", (c) => oauth.handleLogin(c.req.raw)); 18 + app.get("/oauth/callback", (c) => oauth.handleCallback(c.req.raw)); 19 + app.get("/oauth-client-metadata.json", () => oauth.handleClientMetadata()); 20 + app.post("/api/auth/logout", async (c) => { 21 + const res = await oauth.handleLogout(c.req.raw) 22 + const { success } = await res.json(); 23 + if (success) { 24 + return c.html(<LoginForm />) 25 + } 26 + else { 27 + return c.html(<p hx-swap-oob="innerHTML:#error">Error: Logout unsuccessful</p>) 28 + } 29 + }); 30 + 31 + // Routes 32 + app.get("/", (c) => { 33 + return c.html( 34 + <SiteLayout context={c}> 35 + <h1>Pedro</h1> 36 + <p>Ask questions, share opinions, vote together</p> 37 + </SiteLayout> 38 + ) 39 + }); 40 + 41 + // Protected route example 42 + app.get("/profile", async (c) => { 43 + const { session, error } = await oauth.getSessionFromRequest( 44 + c.req.raw, 45 + ); 46 + 47 + if (!session || error) { 48 + return c.html( 49 + <SiteLayout context={c}> 50 + <p>No profile logged in</p> 51 + </SiteLayout> 52 + ) 53 + } 54 + 55 + return c.html( 56 + <SiteLayout context={c}> 57 + <p>PDS: {session.pdsUrl}</p> 58 + <p>DID: {session.did}</p> 59 + </SiteLayout> 60 + ); 61 + }); 62 + 63 + Deno.serve(app.fetch);