Skip to content

Commit

Permalink
fix: enhance oauth/authorization UI
Browse files Browse the repository at this point in the history
  • Loading branch information
sor4chi committed Nov 9, 2024
1 parent 4dda069 commit 8e82984
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 36 deletions.
34 changes: 34 additions & 0 deletions webapp/api/_templates/button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { html } from 'hono/html'

interface ButtonProps {
text: string
variant?: 'primary' | 'secondary'
attributes?: Record<string, string>
}

const flattenAttributes = (attributes: Record<string, string>) =>
Object.entries(attributes)
.map(([key, value]) => `${key}="${value}"`)
.join(' ')

const buttonVariants = {
primary:
'bg-green-600 border-green-600 text-white hover:bg-white hover:text-green-600 transition-colors',
secondary:
'bg-white border-green-600 text-green-600 hover:bg-green-600 hover:text-white transition-colors',
} as const

export const _Button = ({
text,
variant = 'primary',
attributes,
}: ButtonProps) => html`
<button
class="px-4 py-2 rounded-lg font-bold focus:outline-none border-2 w-full ${buttonVariants[
variant
]}"
${attributes ? flattenAttributes(attributes) : ''}
>
${text}
</button>
`
35 changes: 35 additions & 0 deletions webapp/api/_templates/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { html } from 'hono/html'
import { HtmlEscapedString } from 'hono/utils/html'

interface LayoutProps {
children: HtmlEscapedString | Promise<HtmlEscapedString>
subtitle?: string
}

const TITLE = 'Maximum Auth'
const ORG_NAME = '埼玉大学 プログラミングサークル Maximum'

export const _Layout = ({ subtitle, children }: LayoutProps) => html`
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>${subtitle ? `${subtitle} | ${TITLE}` : TITLE}</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<main
class="flex flex-col items-center justify-center p-4 w-screen h-screen"
>
${children}
</main>
<footer
class="text-sm text-gray-500 absolute bottom-2 left-1/2 transform -translate-x-1/2"
>
&copy; ${new Date().getFullYear()} ${ORG_NAME}
</footer>
</body>
</html>
`
84 changes: 84 additions & 0 deletions webapp/api/oauth/_templates/authorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { _Button } from 'api/_templates/button'
import { html } from 'hono/html'

interface AuthorizeProps {
appName: string
appOwnerName: string
scopes: { name: string; description: string | null }[]
oauthFields: {
clientId: string
redirectUri: string
state: string
scope: string
token: string
nowUnixMs: number
}
}

export const _Authorize = ({
appName,
appOwnerName,
scopes,
oauthFields,
}: AuthorizeProps) => html`
<div class="max-w-md space-y-8">
<div>
<h1 class="text-3xl font-bold mb-2 text-center">${appName}</h1>
<span class="block text-lg font-normal text-gray-600 text-center">
を承認しますか?
</span>
</div>
<div class="space-y-6">
<p class="text-md text-gray-800 text-center">
承認すると ${appName} は以下の情報にアクセスできるようになります。
</p>
<div class="bg-gray-50 p-4 rounded-lg">
<table class="border-collapse table-auto w-full text-sm">
<tbody>
${scopes.map(
data => html`
<tr class="[&:not(:last-child)]:border-b-[1px] border-gray-200">
<td class="px-4 py-2 font-medium">${data.name}</td>
<td class="px-4 py-2 font-normal text-gray-500">
${data.description}
</td>
</tr>
`,
)}
</tbody>
</table>
</div>
<form method="POST" action="/oauth/callback" class="space-y-4">
<input type="hidden" name="client_id" value="${oauthFields.clientId}" />
<input
type="hidden"
name="redirect_uri"
value="${oauthFields.redirectUri}"
/>
<input type="hidden" name="state" value="${oauthFields.state}" />
<input type="hidden" name="scope" value="${oauthFields.scope}" />
<input type="hidden" name="time" value="${oauthFields.nowUnixMs}" />
<input type="hidden" name="auth_token" value="${oauthFields.token}" />
<div class="flex justify-around gap-4">
${_Button({
text: '承認する',
variant: 'primary',
attributes: { type: 'submit', name: 'authorized', value: '1' },
})}
${_Button({
text: '拒否する',
variant: 'secondary',
attributes: { type: 'submit', name: 'authorized', value: '0' },
})}
</div>
<p class="text-sm text-gray-600 mt-2 text-center">
${appOwnerName} によってリクエストされました。
</p>
</form>
</div>
<p class="text-sm text-gray-600 text-center">
${new URL(oauthFields.redirectUri).origin} へリダイレクトします。
このアドレスが意図しているものか確認してください。
</p>
</div>
`
58 changes: 22 additions & 36 deletions webapp/api/oauth/authorize.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { importKey } from '@saitamau-maximum/auth/internal'
import { _Layout } from 'api/_templates/layout'
import { Context, Hono } from 'hono'
import { html } from 'hono/html'
import { validator } from 'hono/validator'
import { HonoEnv } from 'load-context'
import { generateAuthToken } from 'utils/auth-token.server'
import { z } from 'zod'

import { _Authorize } from './_templates/authorize'

const app = new Hono<HonoEnv>()

// 仕様はここ参照: https://github.com/saitamau-maximum/auth/issues/27
Expand Down Expand Up @@ -185,41 +187,25 @@ app.get(
key: privateKey,
})

// TODO: デザインちゃんとする
// とりあえず GitHub OAuth のイメージで書いてる
const responseHtml = html`<!doctype html>
<html lang="ja">
<head>
<title>Authorize ${clientInfo.name} | Maximum Auth</title>
</head>
<body>
<h1>${clientInfo.name} を承認しますか?</h1>
<div>
承認すると、 ${clientInfo.owner.displayName} による
${clientInfo.name} はあなたのアカウント
(ここにログインユーザーの情報を入れる)
の以下の情報にアクセスできるようになります。
<ul>
${clientInfo.scopes.map(
data =>
html`<li>${data.scope.name}: ${data.scope.description}</li>`,
)}
</ul>
</div>
<form method="POST" action="/oauth/callback">
<input type="hidden" name="client_id" value="${clientId}" />
<input type="hidden" name="redirect_uri" value="${redirectUri}" />
<input type="hidden" name="state" value="${state}" />
<input type="hidden" name="scope" value="${scope}" />
<input type="hidden" name="time" value="${nowUnixMs}" />
<input type="hidden" name="auth_token" value="${token}" />
<button type="submit" name="authorized" value="1">承認する</button>
<button type="submit" name="authorized" value="0">拒否する</button>
${new URL(redirectUri).origin} にリダイレクトします。
このアドレスが意図しているものか確認してください。
</form>
</body>
</html> `
const responseHtml = _Layout({
children: _Authorize({
appName: clientInfo.name,
appOwnerName: clientInfo.owner.displayName,
scopes: clientInfo.scopes.map(data => ({
name: data.scope.name,
description: data.scope.description,
})),
oauthFields: {
clientId,
redirectUri,
state,
scope,
token,
nowUnixMs,
},
}),
subtitle: clientInfo.name,
})

c.header('Cache-Control', 'no-store')
c.header('Pragma', 'no-cache')
Expand Down

0 comments on commit 8e82984

Please sign in to comment.