mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 13:48:31 +00:00
fix: remove the email field
This commit is contained in:
+1
-1
@@ -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,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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user