fix: remove the email field

This commit is contained in:
2026-08-27 20:41:45 +07:00
parent 3a497fd539
commit a42af43ee7
13 changed files with 233 additions and 112 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ export function LandingPage() {
</div>
<div className="nav-actions">
{sessionQuery.data?.user ? (
{sessionQuery.data?.authenticated ? (
<button
className="nav-auth"
type="button"
+1 -7
View File
@@ -1,16 +1,10 @@
import { getJson, postJson } from "./http";
export type AuthUser = {
id: string;
email: string;
};
export type SessionResponse = {
user: AuthUser | null;
authenticated: boolean;
};
export type LoginInput = {
email: string;
password: string;
};
+5 -18
View File
@@ -3,19 +3,18 @@ import { navigate } from "../lib/router";
import { useLoginMutation, useSessionQuery } from "../queries/auth";
export function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const sessionQuery = useSessionQuery();
const loginMutation = useLoginMutation();
useEffect(() => {
if (sessionQuery.data?.user) navigate("/", { replace: true });
}, [sessionQuery.data?.user]);
if (sessionQuery.data?.authenticated) navigate("/", { replace: true });
}, [sessionQuery.data?.authenticated]);
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
loginMutation.mutate(
{ email, password },
{ password },
{ onSuccess: () => navigate("/", { replace: true }) },
);
}
@@ -38,23 +37,10 @@ export function LoginPage() {
<div className="auth-heading">
<p>Admin access</p>
<h1 id="login-title">Sign in to Upwatch</h1>
<span>Manage monitors and review incidents from one place.</span>
<span>Enter the admin password to manage your monitors.</span>
</div>
<form className="auth-form" onSubmit={handleSubmit}>
<div className="auth-field">
<label htmlFor="email">Email address</label>
<input
id="email"
type="email"
autoComplete="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
autoFocus
/>
</div>
<div className="auth-field">
<label htmlFor="password">Password</label>
<input
@@ -65,6 +51,7 @@ export function LoginPage() {
onChange={(event) => setPassword(event.target.value)}
minLength={8}
required
autoFocus
/>
</div>
+1 -1
View File
@@ -32,7 +32,7 @@ export function useLogoutMutation() {
return useMutation({
mutationFn: logout,
onSuccess: () => {
queryClient.setQueryData<SessionResponse>(authKeys.session(), { user: null });
queryClient.setQueryData<SessionResponse>(authKeys.session(), { authenticated: false });
},
});
}
+7 -15
View File
@@ -1,24 +1,16 @@
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable(
"users",
{
id: text("id").primaryKey(),
email: text("email").notNull(),
passwordHash: text("password_hash").notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
},
(table) => [uniqueIndex("users_email_unique").on(table.email)],
);
export const adminCredentials = sqliteTable("admin_credentials", {
id: integer("id").primaryKey(),
passwordHash: text("password_hash").notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
});
export const sessions = sqliteTable(
"sessions",
{
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
userAgent: text("user_agent"),
+5 -5
View File
@@ -1,10 +1,10 @@
import { getCookie } from "hono/cookie";
import { createMiddleware } from "hono/factory";
import { getDb } from "../db/client";
import { getSessionUser, SESSION_COOKIE, type SessionUser } from "./session";
import { hasValidSession, SESSION_COOKIE } from "./session";
export type AuthVariables = {
user: SessionUser;
authenticated: true;
};
export const requireAuth = createMiddleware<{
@@ -14,9 +14,9 @@ export const requireAuth = createMiddleware<{
const token = getCookie(context, SESSION_COOKIE);
if (!token) return context.json({ message: "Authentication required" }, 401);
const user = await getSessionUser(getDb(context.env), token);
if (!user) return context.json({ message: "Authentication required" }, 401);
const authenticated = await hasValidSession(getDb(context.env), token);
if (!authenticated) return context.json({ message: "Authentication required" }, 401);
context.set("user", user);
context.set("authenticated", true);
await next();
});
+5 -13
View File
@@ -1,16 +1,11 @@
import { and, eq, gt } from "drizzle-orm";
import type { CookieOptions } from "hono/utils/cookie";
import type { Database } from "../db/client";
import { sessions, users } from "../db/schema";
import { sessions } from "../db/schema";
export const SESSION_COOKIE = "upwatch_session";
export const SESSION_DURATION_SECONDS = 7 * 24 * 60 * 60;
export type SessionUser = {
id: string;
email: string;
};
function bytesToBase64Url(bytes: Uint8Array) {
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
@@ -36,7 +31,6 @@ export function sessionCookieOptions(requestUrl: string): CookieOptions {
export async function createSession(
db: Database,
userId: string,
userAgent: string | null,
) {
const token = bytesToBase64Url(crypto.getRandomValues(new Uint8Array(32)));
@@ -44,7 +38,6 @@ export async function createSession(
await db.insert(sessions).values({
id: await sha256Hex(token),
userId,
createdAt: now,
expiresAt: new Date(now.getTime() + SESSION_DURATION_SECONDS * 1000),
userAgent,
@@ -53,14 +46,13 @@ export async function createSession(
return token;
}
export async function getSessionUser(
export async function hasValidSession(
db: Database,
token: string,
): Promise<SessionUser | null> {
): Promise<boolean> {
const [result] = await db
.select({ id: users.id, email: users.email })
.select({ id: sessions.id })
.from(sessions)
.innerJoin(users, eq(sessions.userId, users.id))
.where(
and(
eq(sessions.id, await sha256Hex(token)),
@@ -69,7 +61,7 @@ export async function getSessionUser(
)
.limit(1);
return result ?? null;
return result !== undefined;
}
export async function revokeSession(db: Database, token: string) {
+17 -22
View File
@@ -2,12 +2,12 @@ import { and, count, eq, gte } from "drizzle-orm";
import { Hono } from "hono";
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
import { getDb } from "../db/client";
import { loginAttempts, users } from "../db/schema";
import { adminCredentials, loginAttempts } from "../db/schema";
import { verifyPassword } from "../lib/password";
import { requireAuth, type AuthVariables } from "../lib/require-auth";
import {
createSession,
getSessionUser,
hasValidSession,
revokeSession,
SESSION_COOKIE,
sessionCookieOptions,
@@ -15,10 +15,9 @@ import {
const LOGIN_WINDOW_MS = 15 * 60 * 1000;
const MAX_FAILED_ATTEMPTS = 10;
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const ADMIN_CREDENTIAL_ID = 1;
type LoginBody = {
email?: unknown;
password?: unknown;
};
@@ -33,13 +32,11 @@ authRoutes.post("/api/auth/login", async (context) => {
}
if (
typeof body.email !== "string" ||
!EMAIL_PATTERN.test(body.email.trim()) ||
typeof body.password !== "string" ||
body.password.length < 8
) {
return context.json(
{ message: "Enter a valid email and a password of at least 8 characters" },
{ message: "Enter a password of at least 8 characters" },
400,
);
}
@@ -64,34 +61,30 @@ authRoutes.post("/api/auth/login", async (context) => {
);
}
const email = body.email.trim().toLowerCase();
const [user] = await db
const [credential] = await db
.select({
id: users.id,
email: users.email,
passwordHash: users.passwordHash,
passwordHash: adminCredentials.passwordHash,
})
.from(users)
.where(eq(users.email, email))
.from(adminCredentials)
.where(eq(adminCredentials.id, ADMIN_CREDENTIAL_ID))
.limit(1);
const passwordMatches = user
? await verifyPassword(body.password, user.passwordHash)
const passwordMatches = credential
? await verifyPassword(body.password, credential.passwordHash)
: false;
if (!user || !passwordMatches) {
if (!credential || !passwordMatches) {
await db.insert(loginAttempts).values({ ipAddress, attemptedAt: new Date() });
return context.json({ message: "Email or password is incorrect" }, 401);
return context.json({ message: "Password is incorrect" }, 401);
}
await db.delete(loginAttempts).where(eq(loginAttempts.ipAddress, ipAddress));
const token = await createSession(
db,
user.id,
context.req.header("User-Agent") ?? null,
);
setCookie(context, SESSION_COOKIE, token, sessionCookieOptions(context.req.url));
return context.json({ user: { id: user.id, email: user.email } });
return context.json({ authenticated: true });
});
authRoutes.post("/api/auth/logout", requireAuth, async (context) => {
@@ -107,8 +100,10 @@ authRoutes.post("/api/auth/logout", requireAuth, async (context) => {
authRoutes.get("/api/auth/me", async (context) => {
const token = getCookie(context, SESSION_COOKIE);
const user = token ? await getSessionUser(getDb(context.env), token) : null;
return context.json({ user });
const authenticated = token
? await hasValidSession(getDb(context.env), token)
: false;
return context.json({ authenticated });
});
export default authRoutes;