diff --git a/api/actions/admin.ts b/api/actions/admin.ts index 584ef4a..57f73c8 100644 --- a/api/actions/admin.ts +++ b/api/actions/admin.ts @@ -26,11 +26,12 @@ export async function listUsers(ctx: Context) { admins(ctx) return (await db .selectFrom('user') - .select(['nick', 'email', 'emailVerified', 'oauthGithub', 'createdAt', 'updatedAt']) + .selectAll() .execute() - ).map(item => - [item.nick, item] as const - ) + ).map(item => { + item.password = '✓' // mask password + return [item.nick, item] as const + }) } actions.post.deleteUser = deleteUser diff --git a/api/actions/login-register.ts b/api/actions/login-register.ts index 4af75d5..c709185 100644 --- a/api/actions/login-register.ts +++ b/api/actions/login-register.ts @@ -251,13 +251,13 @@ export async function logout(ctx: Context) { } actions.post.forgotPassword = forgotPassword -export async function forgotPassword(ctx: Context, nickOrEmail: string) { - ctx.log('Forgot password for:', nickOrEmail) +export async function forgotPassword(ctx: Context, email: string) { + ctx.log('Forgot password for:', email) - const user = await getUser(nickOrEmail) + const user = await getUserByEmail(email) if (!user) { - ctx.log('Forgot password user does not exist:', nickOrEmail) + ctx.log('Forgot password user does not exist:', email) // fake a delay that would have been a call to an email service await timeout(2000 + Math.random() * 5000) return @@ -295,15 +295,14 @@ If you did not request a password reset, simply ignore this email.`, } } -actions.get.getResetPasswordUser = getResetPasswordUser -export async function getResetPasswordUser(_ctx: Context, token: string) { +actions.get.getResetPasswordUserNick = getResetPasswordUserNick +export async function getResetPasswordUserNick(_ctx: Context, token: string) { const result = await kv.get(['resetPassword', token]) if (result.value) { const user = await getUserByNick(result.value) if (user) { - user.password = null - return user + return user.nick } } diff --git a/api/schemas/user.ts b/api/schemas/user.ts index 5ca112e..668898b 100644 --- a/api/schemas/user.ts +++ b/api/schemas/user.ts @@ -24,6 +24,11 @@ export const UserLogin = z.object({ password: z.string(), }) +export type UserForgot = z.infer +export const UserForgot = z.object({ + email: z.string(), +}) + export type UserSession = z.infer export const UserSession = z.object({ nick: z.string(), diff --git a/src/comp/Login.tsx b/src/comp/Login.tsx index 6ca7997..4aac3b5 100644 --- a/src/comp/Login.tsx +++ b/src/comp/Login.tsx @@ -1,62 +1,106 @@ -import { refs, Sigui } from 'sigui' -import { UserLogin } from '../../api/schemas/user.ts' +import { Sigui } from 'sigui' +import { UserForgot, UserLogin } from '../../api/schemas/user.ts' import * as actions from '../rpc/login-register.ts' import { parseForm } from '../util/parse-form.ts' +import { Link } from '../ui/Link.tsx' +import { Input, Label } from '../ui/index.ts' export function Login() { using $ = Sigui() const info = $({ + mode: 'login' as 'login' | 'forgot', + forgotEmail: null as null | string, error: '' }) - function onSubmit(ev: Event & { target: HTMLFormElement, submitter: HTMLElement }) { + function submitLogin(ev: Event & { target: HTMLFormElement, submitter: HTMLElement }) { ev.preventDefault() const userLogin = parseForm(ev.target, UserLogin) + actions + .login(userLogin) + .then(actions.loginUser) + .catch(err => info.error = err.message) + return false + } - if (ev.submitter === refs.forgot) { - actions - .forgotPassword(userLogin.nickOrEmail) - .then(() => { - alert('Check your email for a password reset link.') - }) - } - else { - actions - .login(userLogin) - .then(actions.loginUser) - .catch(err => info.error = err.message) - } + function submitForgot(ev: Event & { target: HTMLFormElement, submitter: HTMLElement }) { + ev.preventDefault() + const userForgot = parseForm(ev.target, UserForgot) + actions + .forgotPassword(userForgot.email) + .then(() => { + info.forgotEmail = userForgot.email + }) return false } - return
- + return
+ {() => { + switch (info.mode) { + case 'login': + return +

Login

+ +
+ + + + +
+ info.mode = 'forgot'}>Forgot password + +
-
+ {() => info.error} +
- + -
+ case 'forgot': + if (info.forgotEmail) { + const site = `https://${info.forgotEmail.split('@')[1]}` + return
Done! Check your email for a reset password link.
+ } + else { + return
+

Forgot Password

- +
+ - +
+ + or info.mode = 'login'}>login using password +
- {() => info.error} - + {() => info.error} +
+ + } + } + }} +
} diff --git a/src/comp/OAuthLogin.tsx b/src/comp/OAuthLogin.tsx new file mode 100644 index 0000000..a0217ac --- /dev/null +++ b/src/comp/OAuthLogin.tsx @@ -0,0 +1,34 @@ +import { on } from 'utils' +import { whoami } from '../rpc/login-register.ts' +import { state } from '../state.ts' + +export function OAuthLogin() { + function oauthLogin(provider: string) { + const h = 700 + const w = 500 + const x = window.outerWidth / 2 + window.screenX - (w / 2) + const y = window.outerHeight / 2 + window.screenY - (h / 2) + + const url = new URL(`${location.origin}/oauth/popup`) + url.searchParams.set('provider', provider) + const popup = window.open( + url, + 'oauth', + `width=${w}, height=${h}, top=${y}, left=${x}` + ) + + if (!popup) alert('Something went wrong') + + on(window, 'storage', () => { + popup!.close() + if (localStorage.oauth?.startsWith('complete')) { + whoami().then(user => state.user = user) + } + else { + alert('OAuth failed.\n\nTry logging in using a different method.') + } + }, { once: true }) + } + + return +} diff --git a/src/comp/Register.tsx b/src/comp/Register.tsx index 68b00c0..4e1e707 100644 --- a/src/comp/Register.tsx +++ b/src/comp/Register.tsx @@ -1,6 +1,7 @@ import { Sigui } from 'sigui' import { UserRegister } from '../../api/schemas/user.ts' import * as actions from '../rpc/login-register.ts' +import { Input, Label } from '../ui/index.ts' import { parseForm } from '../util/parse-form.ts' export function Register() { @@ -20,26 +21,42 @@ export function Register() { } return
- +

Register

+ +
+ + + + + + +
+ +
+ + {() => info.error} +
-
- - - -
- - - -
- - - - {() => info.error}
} diff --git a/src/comp/ResetPassword.tsx b/src/comp/ResetPassword.tsx index ad2db1f..c2435d8 100644 --- a/src/comp/ResetPassword.tsx +++ b/src/comp/ResetPassword.tsx @@ -1,14 +1,14 @@ import { Sigui } from 'sigui' -import { UserResetPassword, type User } from '../../api/schemas/user.ts' +import { UserResetPassword } from '../../api/schemas/user.ts' import * as actions from '../rpc/login-register.ts' -import { parseForm } from '../util/parse-form.ts' import { go } from '../ui/Link.tsx' +import { parseForm } from '../util/parse-form.ts' export function ResetPassword() { using $ = Sigui() const info = $({ - user: null as User | null, + nick: null as null | string, error: '' }) @@ -17,8 +17,10 @@ export function ResetPassword() { const { token, password } = parseForm(ev.target, UserResetPassword) actions .changePassword(token, password) - .then(actions.loginUser) - .then(() => go('/')) + .then(session => { + go('/') + actions.loginUser(session) + }) .catch(err => info.error = err.message) return false } @@ -27,14 +29,14 @@ export function ResetPassword() { if (!token) return
Token not found
actions - .getResetPasswordUser(token) - .then(user => info.user = user) + .getResetPasswordUserNick(token) + .then(nick => info.nick = nick) .catch(err => info.error = err.message) - return
{() => info.user ?
+ return
{() => info.nick ?

Reset Password

- Hello {info.user.nick}! Please enter your new password below: + Hello {info.nick}! Please enter your new password below:
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index e7e241d..156a898 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,48 +1,19 @@ -import { on } from 'utils' import { Login } from '../comp/Login.tsx' import { Logout } from '../comp/Logout.tsx' import { Register } from '../comp/Register.tsx' -import { whoami } from '../rpc/login-register.ts' import { state } from '../state.ts' import { Link } from '../ui/Link.tsx' export function Home() { - function oauthLogin(provider: string) { - const h = 700 - const w = 500 - const x = window.outerWidth / 2 + window.screenX - (w / 2) - const y = window.outerHeight / 2 + window.screenY - (h / 2) - - const url = new URL(`${location.origin}/oauth/popup`) - url.searchParams.set('provider', provider) - const popup = window.open( - url, - 'oauth', - `width=${w}, height=${h}, top=${y}, left=${x}` - ) - - if (!popup) alert('Something went wrong') - - on(window, 'storage', () => { - popup!.close() - if (localStorage.oauth?.startsWith('complete')) { - whoami().then(user => state.user = user) - } - else { - alert('OAuth failed.\n\nTry logging in using a different method.') - } - }, { once: true }) - } - return
{() => state.user === undefined ?
Loading...
: state.user === null ? -
+
+ or -
:
diff --git a/src/rpc/login-register.ts b/src/rpc/login-register.ts index a8c9480..5fa7c15 100644 --- a/src/rpc/login-register.ts +++ b/src/rpc/login-register.ts @@ -13,7 +13,7 @@ export const sendVerificationEmail = rpc(' export const verifyEmail = rpc('POST', 'verifyEmail') export const forgotPassword = rpc('POST', 'forgotPassword') -export const getResetPasswordUser = rpc('GET', 'getResetPasswordUser') +export const getResetPasswordUserNick = rpc('GET', 'getResetPasswordUserNick') export const changePassword = rpc('POST', 'changePassword') export function loginUser(session: UserSession) { diff --git a/src/ui/Input.tsx b/src/ui/Input.tsx new file mode 100644 index 0000000..2323d70 --- /dev/null +++ b/src/ui/Input.tsx @@ -0,0 +1,6 @@ +export function Input(props: Record) { + return +} diff --git a/src/ui/Label.tsx b/src/ui/Label.tsx new file mode 100644 index 0000000..d401ed3 --- /dev/null +++ b/src/ui/Label.tsx @@ -0,0 +1,8 @@ +export function Label({ text, children }: { text: string, children?: any }) { + return +} diff --git a/src/ui/Link.tsx b/src/ui/Link.tsx index 48a16a8..8417112 100644 --- a/src/ui/Link.tsx +++ b/src/ui/Link.tsx @@ -13,9 +13,17 @@ export function go(href: string) { link.url = new URL(location.href) } -export function Link({ href, children }: { href: string, children?: any }) { +export function Link({ + href = '#', + onclick = go, + children +}: { + href?: string, + onclick?: (href: string) => unknown, + children?: any +}) { return { ev.preventDefault() - go(href) + onclick(href) }}>{children} } diff --git a/src/ui/index.ts b/src/ui/index.ts new file mode 100644 index 0000000..91c11b5 --- /dev/null +++ b/src/ui/index.ts @@ -0,0 +1,3 @@ +export * from './Input.tsx' +export * from './Label.tsx' +export * from './Link.tsx' diff --git a/src/util/parse-form.ts b/src/util/parse-form.ts index e372531..0ff0328 100644 --- a/src/util/parse-form.ts +++ b/src/util/parse-form.ts @@ -1,6 +1,6 @@ import type { z } from 'zod' -export function parseForm(el: HTMLFormElement, schema: T) { +export function parseForm(el: HTMLFormElement, schema: T) { const form = new FormData(el) const data = Object.fromEntries(form.entries()) return schema.parse(data) as z.infer diff --git a/style.css b/style.css index f7f277f..2ac8175 100644 --- a/style.css +++ b/style.css @@ -2,13 +2,25 @@ @tailwind components; @tailwind utilities; +@layer base { + h1 { + @apply text-2xl; + } + h2 { + @apply text-xl; + } + h3 { + @apply text-lg; + } +} + body { background: #1a1a1a; color: #aaa; font-family: sans-serif; } button, input { - height: 30px; + min-height: 30px; padding: 0 0.25rem; background: #444; color: #ccc;