data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

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

fix: actually validate oauth sob

+116 -27
+83
src/lib/guestbookAuth.ts
··· 6 6 7 7 export const callbackUrl = `${PUBLIC_BASE_URL}/guestbook/` 8 8 9 + interface TokenResponse { 10 + accessToken: string, 11 + tokenType: string, 12 + scope: string, 13 + } 14 + 9 15 export const discord = { 16 + name: 'discord', 10 17 getAuthUrl: (state: string, scopes: string[] = []) => { 11 18 const client_id = env.DISCORD_CLIENT_ID 12 19 const redir_uri = encodeURIComponent(callbackUrl) 13 20 const scope = scopes.join("+") 14 21 return `https://discord.com/oauth2/authorize?client_id=${client_id}&response_type=code&redirect_uri=${redir_uri}&scope=${scope}&state=${state}` 22 + }, 23 + getToken: async (code: string): Promise<TokenResponse> => { 24 + const api = `https://discord.com/api/oauth2/token` 25 + const body = new URLSearchParams({ 26 + client_id: env.DISCORD_CLIENT_ID, 27 + client_secret: env.DISCORD_CLIENT_SECRET, 28 + grant_type: 'authorization_code', 29 + redirect_uri: callbackUrl, 30 + code, 31 + }) 32 + const resp = await fetch(api, { method: 'POST', body }) 33 + if (resp.status !== 200) { 34 + throw new Error("woopsies, couldnt get oauth token") 35 + } 36 + const tokenResp: any = await resp.json() 37 + return { 38 + accessToken: tokenResp.access_token, 39 + tokenType: tokenResp.token_type, 40 + scope: tokenResp.scope, 41 + } 42 + }, 43 + identifyToken: async (tokenResp: TokenResponse): Promise<string> => { 44 + const api = `https://discord.com/api/users/@me` 45 + const resp = await fetch(api, {headers: { 46 + 'Authorization': `${tokenResp.tokenType} ${tokenResp.accessToken}` 47 + }}) 48 + if (resp.status !== 200) { 49 + throw new Error("woopsies, couldnt validate access token") 50 + } 51 + const body = await resp.json() 52 + return body.username 15 53 } 16 54 } 17 55 export const github = { 56 + name: 'github', 18 57 getAuthUrl: (state: string, scopes: string[] = []) => { 19 58 const client_id = env.GITHUB_CLIENT_ID 20 59 const redir_uri = encodeURIComponent(callbackUrl) 21 60 const scope = encodeURIComponent(scopes.join(" ")) 22 61 return `https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=${redir_uri}&scope=${scope}&state=${state}` 62 + }, 63 + getToken: async (code: string): Promise<TokenResponse> => { 64 + const api = `https://discord.com/api/oauth2/token` 65 + const body = new URLSearchParams({ 66 + client_id: env.GITHUB_CLIENT_ID, 67 + client_secret: env.GITHUB_CLIENT_SECRET, 68 + redirect_uri: callbackUrl, 69 + code, 70 + }) 71 + const resp = await fetch(api, { method: 'POST', body, headers: { 'Accept': 'application/json' } }) 72 + if (resp.status !== 200) { 73 + throw new Error("woopsies, couldnt get oauth token") 74 + } 75 + const tokenResp: any = await resp.json() 76 + return { 77 + accessToken: tokenResp.access_token, 78 + tokenType: tokenResp.token_type, 79 + scope: tokenResp.scope, 80 + } 81 + }, 82 + identifyToken: async (tokenResp: TokenResponse): Promise<string> => { 83 + const api = `https://api.github.com/user` 84 + const resp = await fetch(api, {headers: { 85 + 'Authorization': `${tokenResp.tokenType} ${tokenResp.accessToken}` 86 + }}) 87 + if (resp.status !== 200) { 88 + throw new Error("woopsies, couldnt validate access token") 89 + } 90 + const body = await resp.json() 91 + return body.login 23 92 } 24 93 } 25 94 ··· 57 126 return code 58 127 } 59 128 129 + export const getAuthClient = (name: string) => { 130 + switch (name) { 131 + case "discord": 132 + return discord 133 + 134 + case "github": 135 + return github 136 + 137 + default: 138 + return null 139 + } 140 + } 141 + 60 142 export default { 61 143 callbackUrl, 62 144 discord, github, 63 145 createAuthUrl, 64 146 extractCode, 147 + getAuthClient, 65 148 }
+32 -14
src/routes/guestbook/+page.server.ts
··· 24 24 25 25 const postAction = (client: any, scopes: string[]) => { 26 26 return async ({ request, cookies }: { request: Request, cookies: Cookies }) => { 27 + const scopedCookies = scopeCookies(cookies) 28 + scopedCookies.set("postAuth", client.name) 27 29 const form = await request.formData() 28 - const author = form.get("author")?.toString().substring(0, 32).replace(/([^_a-z0-9]+)/gi, '') 29 30 const content = form.get("content")?.toString().substring(0, 512) 30 - const scopedCookies = scopeCookies(cookies) 31 - if (author === undefined || content === undefined) { 32 - scopedCookies.set("sendError", "one of author or content fields are missing") 33 - redirect(303, auth.callbackUrl) 34 - } 35 - if (['dusk', 'yusdacra'].includes(author.trim())) { 36 - scopedCookies.set("sendError", "author cannot be dusk or yusdacra (those are my names choose something else smh)") 31 + if (content === undefined) { 32 + scopedCookies.set("sendError", "content field is missing") 37 33 redirect(303, auth.callbackUrl) 38 34 } 39 35 // save form content in a cookie 40 - const params = new URLSearchParams({ author, content }) 36 + const params = new URLSearchParams({ content }) 41 37 scopedCookies.set("postData", params.toString()) 42 38 // get auth url to redirect user to 43 39 const authUrl = auth.createAuthUrl((state) => client.getAuthUrl(state, scopes), cookies) ··· 62 58 getRatelimited: false, 63 59 } 64 60 const rawPostData = scopedCookies.get("postData") || null 65 - if (rawPostData !== null) { 61 + const postAuth = scopedCookies.get("postAuth") || null 62 + if (rawPostData !== null && postAuth !== null) { 66 63 // delete the postData cookie after we got it cause we dont need it anymore 67 64 scopedCookies.delete("postData") 65 + scopedCookies.delete("postAuth") 68 66 // check if we are landing from an auth from a post action 69 67 let code: string | null = null 70 68 // try to get the code, fails if invalid oauth request ··· 73 71 } catch (err: any) { 74 72 data.sendError = err.toString() 75 73 } 76 - // if we do have a code, then actually make the put request to guestbook server 77 - if (code !== null) { 74 + // if we do have a code, then make the access token request 75 + const authClient = auth.getAuthClient(postAuth) 76 + if (authClient !== null && code !== null) { 77 + // get and validate access token, also get username 78 + let author: string 79 + try { 80 + const tokenResp = await authClient.getToken(code) 81 + author = await authClient.identifyToken(tokenResp) 82 + } catch(err: any) { 83 + scopedCookies.set("sendError", `oauth failed: ${err.toString()}`) 84 + redirect(303, auth.callbackUrl) 85 + } 78 86 let respRaw: Response 79 87 try { 80 88 const postData = new URLSearchParams(rawPostData) 81 - respRaw = await fetch(`${GUESTBOOK_BASE_URL}`, { method: 'POST', body: postData }) 89 + // set author to the identified value we got 90 + postData.set('author', author) 91 + // return error if content was not set or if empty 92 + const content = postData.get('content') 93 + if (content === null || content.trim().length === 0) { 94 + scopedCookies.set("sendError", `content field was empty`) 95 + redirect(303, auth.callbackUrl) 96 + } 97 + // set content, make sure to trim it 98 + postData.set('content', content.substring(0, 512).trim()) 99 + respRaw = await fetch(GUESTBOOK_BASE_URL, { method: 'POST', body: postData }) 82 100 } catch (err: any) { 83 101 scopedCookies.set("sendError", `${err.toString()} (is guestbook server running?)`) 84 102 redirect(303, auth.callbackUrl) ··· 97 115 data.page = Math.max(data.page, 1) 98 116 let respRaw: Response 99 117 try { 100 - respRaw = await fetch(GUESTBOOK_BASE_URL + "/" + data.page) 118 + respRaw = await fetch(`${GUESTBOOK_BASE_URL}/${data.page}`) 101 119 } catch (err: any) { 102 120 data.getError = `${err.toString()} (is guestbook server running?)` 103 121 return data
+1 -13
src/routes/guestbook/+page.svelte
··· 16 16 just fill the post in and click on your preferred auth method to post 17 17 </p> 18 18 <p>rules: be a good human bean pretty please</p> 19 - <p> 20 - (note: the author name must only include alphanumerical characters or underscore, and must 21 - be less than 32 characters) 22 - </p> 23 19 <form method="post"> 24 20 <div class="entry entryflex"> 25 21 <div class="flex flex-row"> ··· 34 30 required 35 31 /> 36 32 <p class="place-self-end text-sm font-monospace"> 37 - --- posted by <input 38 - type="text" 39 - name="author" 40 - placeholder="author" 41 - class="p-0 bg-inherit border-hidden max-w-[16ch] text-right text-sm text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content]" 42 - pattern="[_a-zA-Z0-9]+" 43 - maxlength="32" 44 - required 45 - /> 33 + --- posted by ... 46 34 </p> 47 35 </div> 48 36 <div class="entry flex flex-wrap gap-1.5 p-1">