52 |
53 | Authorization endpoint URI: {oauthConfig.authorizationEndpointUri}
54 |
55 |
Token URI: {oauthConfig.tokenUri}
56 |
Scope: {oauthConfig.defaults?.scope}
57 |
Signed in: {JSON.stringify(hasSessionIdCookie)}
58 |
59 | {hasSessionIdCookie
60 | ? Sign out
61 | : Sign in}
62 |
63 |
64 |
65 |
66 | Source code
67 |
68 |
69 |
70 |
71 |
72 | );
73 |
74 | return ctx.render(jsx);
75 | }
76 |
77 | export const signinHandler = async (req: Request) => {
78 | return await signIn(req);
79 | };
80 |
81 | async function getUser(accessToken: string) {
82 | const response = await fetch("https://api.github.com/user", {
83 | headers: {
84 | Authorization: `token ${accessToken}`,
85 | },
86 | });
87 | const data = await response.json();
88 | return data;
89 | }
90 |
91 | const findUserByLogin = (ctx: Context, login: string) => {
92 | const userStore = ctx.stores.get("users");
93 | if (!userStore) return null;
94 | for (const [id, { value }] of userStore.entries()) {
95 | if (value.username === login) {
96 | return { ...value, ...{ id } };
97 | }
98 | }
99 | return null;
100 | };
101 |
102 | export const callbackHandler = async (req: HttpRequest, ctx: Context) => {
103 | try {
104 | const { response, sessionId, tokens } = await handleCallback(
105 | req,
106 | );
107 | const user = await getUser(tokens.accessToken);
108 | const registeredUser = await findUserByLogin(ctx, user.login);
109 | if (!registeredUser) {
110 | const id = ulid();
111 | await ctx.stores.get("users")?.set(id, {
112 | username: user.login,
113 | avatar_url: user.avatar_url,
114 | }).commit();
115 | }
116 |
117 | kv.set(["session", sessionId], {
118 | id: user.id,
119 | avatar_url: user.avatar_url,
120 | login: user.login,
121 | }, { expireIn: DAY });
122 | return response;
123 | } catch {
124 | return new Response(null, { status: STATUS_CODE.InternalServerError });
125 | }
126 | };
127 |
128 | export const signoutHandler = async (req: HttpRequest, ctx: Context) => {
129 | const sessionId = await getSessionId(req);
130 | if (sessionId) {
131 | const store = ctx.stores.get("core");
132 | if (store) {
133 | store.delete(sessionId);
134 | await store.commit();
135 | }
136 | await signOut(req);
137 | return new Response(null, {
138 | status: 302,
139 | headers: {
140 | Location: "/",
141 | },
142 | });
143 | }
144 | };
145 |
146 | /**
147 | * To use this module,
148 | * see the example: https://github.com/fastrodev/fastro/blob/main/examples/oauth.ts
149 | * @param f: Fastro
150 | * @returns Fastro
151 | */
152 | export default function authModule(f: Fastro) {
153 | return f.get("/auth", indexHandler)
154 | .get("/signin", signinHandler)
155 | .get("/callback", callbackHandler)
156 | .get("/signout", signoutHandler);
157 | }
158 |
--------------------------------------------------------------------------------
/modules/blog/blog.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "Memilih Infrastruktur Server Terbaik untuk Aplikasi Inventori Bisnis Kecil",
4 | "url": "/blog/infrastructure",
5 | "date": "04/01/2025"
6 | },
7 | {
8 | "title": "Understanding Go Channels: A Simple Guide with Kitchen Analogies",
9 | "url": "/blog/channel",
10 | "date": "04/01/2025"
11 | },
12 | {
13 | "title": "Understanding Mutex in Go: Race Conditions vs Thread-Safe Code",
14 | "url": "/blog/mutex",
15 | "date": "04/01/2025"
16 | },
17 | {
18 | "title": "sync.WaitGroup vs errgroup in Go: A Complete Comparison Guide",
19 | "url": "/blog/errgroup",
20 | "date": "04/01/2025"
21 | },
22 | {
23 | "title": "Understanding sync.WaitGroup in Go: A Simple Guide with Examples",
24 | "url": "/blog/waitgroup",
25 | "date": "04/01/2025"
26 | },
27 | {
28 | "title": "Startup Success: 3 Key Ingredients",
29 | "url": "/blog/startup",
30 | "date": "03/30/2025"
31 | },
32 | {
33 | "title": "Using Queues to Avoid Race Conditions",
34 | "url": "/blog/queue",
35 | "date": "10/05/2024"
36 | },
37 | {
38 | "title": "Store: Key and Value Map with TTL",
39 | "url": "/blog/store",
40 | "date": "09/17/2024"
41 | },
42 | {
43 | "title": "Collaboration and Profit Sharing",
44 | "url": "/blog/collaboration",
45 | "date": "06/18/2024"
46 | },
47 | {
48 | "title": "Set up Tailwind on Deno",
49 | "url": "/blog/tailwind",
50 | "date": "01/26/2024"
51 | },
52 | {
53 | "title": "Deno KV OAuth Implementation",
54 | "url": "/blog/oauth",
55 | "date": "11/15/2023"
56 | },
57 | {
58 | "title": "Boosting Performance with renderToReadableStream",
59 | "url": "/blog/render_to_readable_stream",
60 | "date": "10/26/2023"
61 | },
62 | {
63 | "title": "Fixing React Version Consistency Issues",
64 | "url": "/blog/react",
65 | "date": "10/22/2023"
66 | },
67 | {
68 | "title": "Preact Integration and Secure Server Side Props",
69 | "url": "/blog/preact_and_encrypted_props",
70 | "date": "08/16/2023"
71 | },
72 | {
73 | "title": "Hello World",
74 | "url": "/blog/hello",
75 | "date": "11/15/2023"
76 | }
77 | ]
78 |
--------------------------------------------------------------------------------
/modules/blog/mod.ts:
--------------------------------------------------------------------------------
1 | import { Fastro } from "@app/mod.ts";
2 | import tocLayout from "@app/modules/toc/toc.layout.tsx";
3 | import tocApp from "@app/modules/toc/toc.page.tsx";
4 | import { getSession } from "@app/utils/session.ts";
5 | import posts from "@app/modules/blog/blog.json" with { type: "json" };
6 |
7 | export default function (s: Fastro) {
8 | s.page("/blog", {
9 | component: tocApp,
10 | layout: tocLayout,
11 | folder: "modules/toc",
12 | handler: async (req, ctx) => {
13 | const ses = await getSession(req, ctx);
14 | return ctx.render({
15 | isLogin: ses?.isLogin,
16 | avatar_url: ses?.avatar_url,
17 | html_url: ses?.html_url,
18 | title: "Blog",
19 | description: "Blog",
20 | destination: "/blog",
21 | posts,
22 | });
23 | },
24 | });
25 |
26 | return s;
27 | }
28 |
--------------------------------------------------------------------------------
/modules/docs/docs.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "Get Started",
4 | "url": "/docs/start"
5 | },
6 | {
7 | "title": "App Structure",
8 | "url": "/docs/structure"
9 | },
10 | {
11 | "title": "Hello World",
12 | "url": "/docs/hello"
13 | },
14 | {
15 | "title": "Hello World Context",
16 | "url": "/docs/hello-context"
17 | },
18 | {
19 | "title": "Hello JSON",
20 | "url": "/docs/json"
21 | },
22 | {
23 | "title": "Routing",
24 | "url": "/docs/route"
25 | },
26 | {
27 | "title": "URL Params",
28 | "url": "/docs/url-params"
29 | },
30 | {
31 | "title": "Query Params",
32 | "url": "/docs/url-query"
33 | },
34 | {
35 | "title": "App Middleware",
36 | "url": "/docs/app-middleware"
37 | },
38 | {
39 | "title": "Route Middleware",
40 | "url": "/docs/route-middleware"
41 | },
42 | {
43 | "title": "Markdown Middleware",
44 | "url": "/docs/markdown"
45 | },
46 | {
47 | "title": "Tailwind Middleware",
48 | "url": "/docs/tailwind"
49 | },
50 | {
51 | "title": "Static File",
52 | "url": "/docs/static"
53 | },
54 | {
55 | "title": "Hello TSX",
56 | "url": "/docs/tsx"
57 | },
58 | {
59 | "title": "TSX Component",
60 | "url": "/docs/tsx-component"
61 | },
62 | {
63 | "title": "Function Component",
64 | "url": "/docs/fn-component"
65 | },
66 | {
67 | "title": "Server Side Rendering",
68 | "url": "/docs/ssr"
69 | },
70 | {
71 | "title": "OAuth",
72 | "url": "/docs/oauth"
73 | },
74 | {
75 | "title": "MySQL",
76 | "url": "/docs/mysql"
77 | },
78 | {
79 | "title": "Postgres",
80 | "url": "/docs/postgres"
81 | },
82 | {
83 | "title": "Redis",
84 | "url": "/docs/redis"
85 | },
86 | {
87 | "title": "Mongo",
88 | "url": "/docs/mongo"
89 | },
90 | {
91 | "title": "Deno KV",
92 | "url": "/docs/kv"
93 | },
94 | {
95 | "title": "Grouping",
96 | "url": "/docs/group"
97 | },
98 | {
99 | "title": "Deployment",
100 | "url": "/docs/deploy"
101 | },
102 | {
103 | "title": "Store",
104 | "url": "/docs/store"
105 | },
106 | {
107 | "title": "Benchmarks",
108 | "url": "/docs/benchmarks"
109 | }
110 | ]
111 |
--------------------------------------------------------------------------------
/modules/docs/docs.layout.tsx:
--------------------------------------------------------------------------------
1 | import { Footer } from "@app/components/footer.tsx";
2 | import Header from "@app/components/header.tsx";
3 | import posts from "@app/modules/docs/docs.json" with { type: "json" };
4 |
5 | export default function (
6 | props: {
7 | CSS: string;
8 | markdown: string;
9 | attrs: Record