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:
@@ -0,0 +1,27 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
DROP TABLE `sessions`;--> statement-breakpoint
|
||||
DROP TABLE `login_attempts`;--> statement-breakpoint
|
||||
DROP TABLE `users`;--> statement-breakpoint
|
||||
CREATE TABLE `admin_credentials` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`password_hash` text NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`user_agent` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `sessions_expires_at_idx` ON `sessions` (`expires_at`);--> statement-breakpoint
|
||||
CREATE TABLE `login_attempts` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`ip_address` text NOT NULL,
|
||||
`attempted_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `login_attempts_ip_attempted_at_idx` ON `login_attempts` (`ip_address`,`attempted_at`);--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "7773aaeb-a97b-4dd2-b5c7-312c83e1d6fb",
|
||||
"prevId": "f37e2597-6362-40a8-9cf9-570efa9be326",
|
||||
"tables": {
|
||||
"admin_credentials": {
|
||||
"name": "admin_credentials",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"login_attempts": {
|
||||
"name": "login_attempts",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"attempted_at": {
|
||||
"name": "attempted_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"login_attempts_ip_attempted_at_idx": {
|
||||
"name": "login_attempts_ip_attempted_at_idx",
|
||||
"columns": [
|
||||
"ip_address",
|
||||
"attempted_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"sessions_expires_at_idx": {
|
||||
"name": "sessions_expires_at_idx",
|
||||
"columns": [
|
||||
"expires_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1787835952782,
|
||||
"tag": "0000_cooing_rumiko_fujikawa",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1787837780070,
|
||||
"tag": "0001_new_betty_brant",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+3
-11
@@ -1,7 +1,5 @@
|
||||
import { hashPassword } from "../src/worker/lib/password";
|
||||
|
||||
const EMAIL_PATTERN = /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
||||
|
||||
async function readPassword() {
|
||||
if (!process.stdin.isTTY) {
|
||||
return (await new Response(Bun.stdin.stream()).text()).trimEnd();
|
||||
@@ -48,12 +46,6 @@ function shellDoubleQuoted(value: string) {
|
||||
return value.replace(/[\\"$`]/g, "\\$&");
|
||||
}
|
||||
|
||||
const email = process.argv[2]?.trim().toLowerCase();
|
||||
if (!email || !EMAIL_PATTERN.test(email)) {
|
||||
console.error("Usage: bun run admin:create <email>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const password = await readPassword();
|
||||
if (password.length < 8) {
|
||||
console.error("Password must contain at least 8 characters.");
|
||||
@@ -62,9 +54,9 @@ if (password.length < 8) {
|
||||
|
||||
const now = Date.now();
|
||||
const statement = [
|
||||
"INSERT OR REPLACE INTO users",
|
||||
"(id, email, password_hash, created_at, updated_at)",
|
||||
`VALUES (${sqlValue(crypto.randomUUID())}, ${sqlValue(email)}, ${sqlValue(await hashPassword(password))}, ${now}, ${now});`,
|
||||
"INSERT OR REPLACE INTO admin_credentials",
|
||||
"(id, password_hash, created_at, updated_at)",
|
||||
`VALUES (1, ${sqlValue(await hashPassword(password))}, ${now}, ${now});`,
|
||||
].join(" ");
|
||||
|
||||
console.log("\nLocal database:");
|
||||
|
||||
+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;
|
||||
|
||||
+12
-19
@@ -2,23 +2,20 @@ import { applyD1Migrations, env, SELF, type D1Migration } from "cloudflare:test"
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { hashPassword } from "../src/worker/lib/password";
|
||||
|
||||
const ADMIN_EMAIL = "admin@example.com";
|
||||
const ADMIN_PASSWORD = "correct-horse-battery-staple";
|
||||
|
||||
async function seedAdmin() {
|
||||
await env.DB.batch([
|
||||
env.DB.prepare("DELETE FROM login_attempts"),
|
||||
env.DB.prepare("DELETE FROM sessions"),
|
||||
env.DB.prepare("DELETE FROM users"),
|
||||
env.DB.prepare("DELETE FROM admin_credentials"),
|
||||
]);
|
||||
|
||||
const now = Date.now();
|
||||
await env.DB.prepare(
|
||||
"INSERT INTO users (id, email, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
"INSERT INTO admin_credentials (id, password_hash, created_at, updated_at) VALUES (1, ?, ?, ?)",
|
||||
)
|
||||
.bind(
|
||||
"test-admin",
|
||||
ADMIN_EMAIL,
|
||||
await hashPassword(ADMIN_PASSWORD),
|
||||
now,
|
||||
now,
|
||||
@@ -34,7 +31,7 @@ function login(password = ADMIN_PASSWORD, ipAddress = "198.51.100.10") {
|
||||
"CF-Connecting-IP": ipAddress,
|
||||
Origin: "https://example.com",
|
||||
},
|
||||
body: JSON.stringify({ email: ADMIN_EMAIL, password }),
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,19 +46,17 @@ describe("authentication", () => {
|
||||
});
|
||||
beforeEach(seedAdmin);
|
||||
|
||||
it("logs in with the admin credentials and creates an HttpOnly session", async () => {
|
||||
it("logs in with the admin password and creates an HttpOnly session", async () => {
|
||||
const response = await login();
|
||||
const body = await response.json<{ user: { id: string; email: string } }>();
|
||||
const body = await response.json<{ authenticated: boolean }>();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("Set-Cookie")).toContain("upwatch_session=");
|
||||
expect(response.headers.get("Set-Cookie")).toContain("HttpOnly");
|
||||
expect(response.headers.get("Set-Cookie")).toContain("SameSite=Lax");
|
||||
expect(body.user).toEqual({ id: "test-admin", email: ADMIN_EMAIL });
|
||||
expect(body).toEqual({ authenticated: true });
|
||||
|
||||
const session = await env.DB.prepare("SELECT id FROM sessions WHERE user_id = ?")
|
||||
.bind("test-admin")
|
||||
.first();
|
||||
const session = await env.DB.prepare("SELECT id FROM sessions").first();
|
||||
expect(session).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -71,14 +66,14 @@ describe("authentication", () => {
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.headers.get("Set-Cookie")).toBeNull();
|
||||
expect(await response.json()).toEqual({
|
||||
message: "Email or password is incorrect",
|
||||
message: "Password is incorrect",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a nullable user from the session endpoint", async () => {
|
||||
it("returns authentication state from the session endpoint", async () => {
|
||||
const anonymousResponse = await SELF.fetch("https://example.com/api/auth/me");
|
||||
expect(anonymousResponse.status).toBe(200);
|
||||
expect(await anonymousResponse.json()).toEqual({ user: null });
|
||||
expect(await anonymousResponse.json()).toEqual({ authenticated: false });
|
||||
|
||||
const loginResponse = await login();
|
||||
const authenticatedResponse = await SELF.fetch("https://example.com/api/auth/me", {
|
||||
@@ -86,9 +81,7 @@ describe("authentication", () => {
|
||||
});
|
||||
|
||||
expect(authenticatedResponse.status).toBe(200);
|
||||
expect(await authenticatedResponse.json()).toEqual({
|
||||
user: { id: "test-admin", email: ADMIN_EMAIL },
|
||||
});
|
||||
expect(await authenticatedResponse.json()).toEqual({ authenticated: true });
|
||||
});
|
||||
|
||||
it("revokes the persisted session on logout", async () => {
|
||||
@@ -111,7 +104,7 @@ describe("authentication", () => {
|
||||
const sessionResponse = await SELF.fetch("https://example.com/api/auth/me", {
|
||||
headers: { Cookie: cookie },
|
||||
});
|
||||
expect(await sessionResponse.json()).toEqual({ user: null });
|
||||
expect(await sessionResponse.json()).toEqual({ authenticated: false });
|
||||
});
|
||||
|
||||
it("rate limits repeated failed login attempts by IP", async () => {
|
||||
|
||||
Reference in New Issue
Block a user