Compare commits
No commits in common. "49c071945d01974adad2ac36d9b78afbb14d03bb" and "455d130f747b5f9d811739044e63b1f6071e8b48" have entirely different histories.
49c071945d
...
455d130f74
@ -57,41 +57,6 @@ export let checkRateLimit = async function checkRateLimit(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a sliding window rate limit is exceeded without incrementing.
|
|
||||||
*
|
|
||||||
* @param key - The unique identifier for the limit.
|
|
||||||
* @param limit - Maximum requests allowed in the window.
|
|
||||||
* @param windowMs - Size of the window in milliseconds.
|
|
||||||
* @returns boolean - true if limit is exceeded (rate limited), false if allowed.
|
|
||||||
*/
|
|
||||||
export let isRateLimited = async function isRateLimited(
|
|
||||||
key: string,
|
|
||||||
limit: number,
|
|
||||||
windowMs: number,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const now = Date.now();
|
|
||||||
const windowStart = now - windowMs;
|
|
||||||
|
|
||||||
const multi = valkey.multi();
|
|
||||||
multi.zremrangebyscore(key, 0, windowStart);
|
|
||||||
multi.zcount(key, "-inf", "+inf");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const results = await multi.exec();
|
|
||||||
if (!results) return false; // fail open or closed depending on preference, returning false here allows it
|
|
||||||
|
|
||||||
const countResult = results[1];
|
|
||||||
if (countResult[0]) throw countResult[0];
|
|
||||||
|
|
||||||
const currentCount = countResult[1] as number;
|
|
||||||
return currentCount >= limit;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[RateLimit] Error checking isRateLimited:", error);
|
|
||||||
return false; // fail open or closed, returning false here
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rateLimitWrapper = {
|
export const rateLimitWrapper = {
|
||||||
get checkRateLimit() {
|
get checkRateLimit() {
|
||||||
return checkRateLimit;
|
return checkRateLimit;
|
||||||
@ -99,10 +64,4 @@ export const rateLimitWrapper = {
|
|||||||
set checkRateLimit(val: any) {
|
set checkRateLimit(val: any) {
|
||||||
checkRateLimit = val;
|
checkRateLimit = val;
|
||||||
},
|
},
|
||||||
get isRateLimited() {
|
|
||||||
return isRateLimited;
|
|
||||||
},
|
|
||||||
set isRateLimited(val: any) {
|
|
||||||
isRateLimited = val;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import {
|
|||||||
} from "../auth-session.ts";
|
} from "../auth-session.ts";
|
||||||
import { getClientIp } from "../middleware.ts";
|
import { getClientIp } from "../middleware.ts";
|
||||||
import { auditWrapper } from "../audit.ts";
|
import { auditWrapper } from "../audit.ts";
|
||||||
import { rateLimitWrapper } from "../ratelimit.ts";
|
|
||||||
import { EventJoinPage } from "../../ui/components/EventJoinPage.tsx";
|
import { EventJoinPage } from "../../ui/components/EventJoinPage.tsx";
|
||||||
import { EventSplashPage } from "../../ui/components/EventSplashPage.tsx";
|
import { EventSplashPage } from "../../ui/components/EventSplashPage.tsx";
|
||||||
|
|
||||||
@ -181,122 +180,49 @@ eventRoutes.post("/api/join", async (c) => {
|
|||||||
if (!code || typeof code !== "string") {
|
if (!code || typeof code !== "string") {
|
||||||
return c.json({ error: "Event code or PIN is required" }, 400);
|
return c.json({ error: "Event code or PIN is required" }, 400);
|
||||||
}
|
}
|
||||||
|
code = code.trim();
|
||||||
const rawNormalized = code.trim().toLowerCase();
|
|
||||||
const pinNormalized = code.trim().replace(/[-\s]/g, "");
|
|
||||||
|
|
||||||
const clientIp = getClientIp(c);
|
|
||||||
const rateLimitKey = `ratelimit:join:fail:${clientIp}`;
|
|
||||||
|
|
||||||
if (await rateLimitWrapper.isRateLimited(rateLimitKey, 5, 60000)) {
|
|
||||||
return c.json({ error: "Too Many Requests" }, 429);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const eventLookup = await sqlWrapper.sql`
|
const result = await sqlWrapper.sql`
|
||||||
SELECT * FROM event_passes
|
UPDATE event_passes
|
||||||
WHERE (LOWER(slug) = ${rawNormalized} OR REPLACE(pin_code, '-', '') = ${pinNormalized})
|
SET seats_claimed = seats_claimed + 1
|
||||||
|
WHERE (slug = ${code} OR pin_code = ${code})
|
||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
AND (expires_at IS NULL OR expires_at > NOW())
|
AND (expires_at IS NULL OR expires_at > NOW())
|
||||||
|
AND (max_seats = 0 OR seats_claimed < max_seats)
|
||||||
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (!eventLookup || eventLookup.length === 0) {
|
if (!result || result.length === 0) {
|
||||||
await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 60000);
|
|
||||||
return c.json(
|
return c.json(
|
||||||
{ error: "Invalid event code or workshop capacity reached" },
|
{ error: "Invalid event code or workshop capacity reached" },
|
||||||
404,
|
404,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = eventLookup[0];
|
const event = result[0];
|
||||||
const user = await getAuthenticatedUser(c);
|
|
||||||
|
|
||||||
if (user && user.username?.startsWith(`guest_${event.slug}_`)) {
|
|
||||||
const ttl = (Number(event.lifespan_hours) || 3) * 3600;
|
|
||||||
await valkey.expire(user.sessionId, ttl);
|
|
||||||
|
|
||||||
deleteCookie(c, "session_id", { path: "/" });
|
|
||||||
const rpID = Deno.env.get("RP_ID");
|
|
||||||
const cookieDomain = getCookieDomain(rpID);
|
|
||||||
|
|
||||||
setCookie(c, "session_id", user.sessionId, {
|
|
||||||
domain: cookieDomain,
|
|
||||||
path: "/",
|
|
||||||
httpOnly: true,
|
|
||||||
secure: true,
|
|
||||||
sameSite: "Lax",
|
|
||||||
maxAge: ttl,
|
|
||||||
});
|
|
||||||
|
|
||||||
let appDomain = "";
|
|
||||||
if (event.app_id) {
|
|
||||||
const apps = await sqlWrapper
|
|
||||||
.sql`SELECT domain FROM apps WHERE id = ${event.app_id}`;
|
|
||||||
if (apps.length > 0) {
|
|
||||||
appDomain = apps[0].domain || "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const redirectUrl = appDomain ? `https://${appDomain}` : "/dashboard";
|
|
||||||
|
|
||||||
if (
|
|
||||||
c.req.header("accept")?.includes("text/html") &&
|
|
||||||
!c.req.header("accept")?.includes("application/json")
|
|
||||||
) {
|
|
||||||
return c.redirect(redirectUrl, 302);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({
|
|
||||||
success: true,
|
|
||||||
sessionId: user.sessionId,
|
|
||||||
token: user.sessionId,
|
|
||||||
guestUuid: user.userId,
|
|
||||||
username: user.username,
|
|
||||||
redirectUrl,
|
|
||||||
reused: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateResult = await sqlWrapper.sql`
|
|
||||||
UPDATE event_passes
|
|
||||||
SET seats_claimed = seats_claimed + 1
|
|
||||||
WHERE id = ${event.id}
|
|
||||||
AND (max_seats = 0 OR seats_claimed < max_seats)
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (!updateResult || updateResult.length === 0) {
|
|
||||||
await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 60000);
|
|
||||||
return c.json(
|
|
||||||
{ error: "Workshop capacity reached" },
|
|
||||||
404,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedEvent = updateResult[0];
|
|
||||||
const guestUuid = crypto.randomUUID();
|
const guestUuid = crypto.randomUUID();
|
||||||
const username = `guest_${updatedEvent.slug}_${updatedEvent.seats_claimed}`;
|
const username = `guest_${event.slug}_${event.seats_claimed}`;
|
||||||
|
|
||||||
await sqlWrapper.sql`
|
await sqlWrapper.sql`
|
||||||
INSERT INTO users (id, username, display_name, account_status)
|
INSERT INTO users (id, username, display_name, account_status)
|
||||||
VALUES (${guestUuid}, ${username}, ${
|
VALUES (${guestUuid}, ${username}, ${event.name + " Attendee"}, 'guest')
|
||||||
updatedEvent.name + " Attendee"
|
|
||||||
}, 'guest')
|
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
||||||
const sessionId = `ay_sess_${encodeHex(randomBytes)}`;
|
const sessionId = `ay_sess_${encodeHex(randomBytes)}`;
|
||||||
const label = `${updatedEvent.name} Seat #${updatedEvent.seats_claimed}`;
|
const label = `${event.name} Seat #${event.seats_claimed}`;
|
||||||
const ttl = (Number(updatedEvent.lifespan_hours) || 3) * 3600;
|
const ttl = (Number(event.lifespan_hours) || 3) * 3600;
|
||||||
|
|
||||||
let customScopes = ["guest", "trial"];
|
let customScopes = ["guest", "trial"];
|
||||||
let appDomain = "";
|
let appDomain = "";
|
||||||
|
|
||||||
if (updatedEvent.app_id) {
|
if (event.app_id) {
|
||||||
const apps = await sqlWrapper
|
const apps = await sqlWrapper
|
||||||
.sql`SELECT name, domain FROM apps WHERE id = ${updatedEvent.app_id}`;
|
.sql`SELECT name, domain FROM apps WHERE id = ${event.app_id}`;
|
||||||
if (apps.length > 0) {
|
if (apps.length > 0) {
|
||||||
customScopes = [`app:${apps[0].name}`, updatedEvent.role || "viewer"];
|
customScopes = [`app:${apps[0].name}`, event.role || "viewer"];
|
||||||
appDomain = apps[0].domain || "";
|
appDomain = apps[0].domain || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,10 +245,10 @@ eventRoutes.post("/api/join", async (c) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
auditWrapper.auditLog(guestUuid, "event_seat_claimed", updatedEvent.id, {
|
auditWrapper.auditLog(guestUuid, "event_seat_claimed", event.id, {
|
||||||
slug: updatedEvent.slug,
|
slug: event.slug,
|
||||||
name: updatedEvent.name,
|
name: event.name,
|
||||||
seatNumber: updatedEvent.seats_claimed,
|
seatNumber: event.seats_claimed,
|
||||||
method: "web",
|
method: "web",
|
||||||
}, getClientIp(c));
|
}, getClientIp(c));
|
||||||
|
|
||||||
@ -363,17 +289,14 @@ eventRoutes.post("/api/join", async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
eventRoutes.get("/join/:slug", async (c) => {
|
eventRoutes.get("/join/:slug", async (c) => {
|
||||||
const slug = c.req.param("slug") || "";
|
const slug = c.req.param("slug");
|
||||||
const format = c.req.query("format") || "html";
|
const format = c.req.query("format") || "html";
|
||||||
|
|
||||||
const rawNormalized = slug.trim().toLowerCase();
|
|
||||||
const pinNormalized = slug.trim().replace(/[-\s]/g, "");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await sqlWrapper.sql`
|
const result = await sqlWrapper.sql`
|
||||||
UPDATE event_passes
|
UPDATE event_passes
|
||||||
SET seats_claimed = seats_claimed + 1
|
SET seats_claimed = seats_claimed + 1
|
||||||
WHERE (LOWER(slug) = ${rawNormalized} OR REPLACE(pin_code, '-', '') = ${pinNormalized})
|
WHERE slug = ${slug}
|
||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
AND (expires_at IS NULL OR expires_at > NOW())
|
AND (expires_at IS NULL OR expires_at > NOW())
|
||||||
AND (max_seats = 0 OR seats_claimed < max_seats)
|
AND (max_seats = 0 OR seats_claimed < max_seats)
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { valkey } from "../valkey.ts";
|
|||||||
import { rateLimitWrapper } from "../ratelimit.ts";
|
import { rateLimitWrapper } from "../ratelimit.ts";
|
||||||
|
|
||||||
rateLimitWrapper.checkRateLimit = () => Promise.resolve(true);
|
rateLimitWrapper.checkRateLimit = () => Promise.resolve(true);
|
||||||
rateLimitWrapper.isRateLimited = () => Promise.resolve(false);
|
|
||||||
|
|
||||||
Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
||||||
await t.step("POST /api/events creates an event pass", async () => {
|
await t.step("POST /api/events creates an event pass", async () => {
|
||||||
@ -66,32 +65,14 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await t.step(
|
await t.step(
|
||||||
"POST /api/join normalizes PIN without hyphens and mints guest session",
|
"POST /api/join redeems PIN / slug and mints guest session",
|
||||||
async () => {
|
async () => {
|
||||||
const originalSql = sqlWrapper.sql;
|
const originalSql = sqlWrapper.sql;
|
||||||
let updateCalled = false;
|
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
||||||
let selectCalled = false;
|
|
||||||
sqlWrapper.sql = ((strings: any, ...values: any[]) => {
|
|
||||||
const query = Array.isArray(strings)
|
const query = Array.isArray(strings)
|
||||||
? strings.join("?")
|
? strings.join("?")
|
||||||
: String(strings);
|
: String(strings);
|
||||||
if (query.includes("SELECT * FROM event_passes")) {
|
|
||||||
selectCalled = true;
|
|
||||||
// Verify pinNormalized parameter value
|
|
||||||
assert(values.includes("749123"));
|
|
||||||
return Promise.resolve([{
|
|
||||||
id: "event-uuid-1",
|
|
||||||
slug: "deno-lab",
|
|
||||||
pin_code: "749-123",
|
|
||||||
name: "Deno Workshop",
|
|
||||||
max_seats: 50,
|
|
||||||
seats_claimed: 0,
|
|
||||||
lifespan_hours: 3,
|
|
||||||
app_id: null,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
if (query.includes("UPDATE event_passes")) {
|
if (query.includes("UPDATE event_passes")) {
|
||||||
updateCalled = true;
|
|
||||||
return Promise.resolve([{
|
return Promise.resolve([{
|
||||||
id: "event-uuid-1",
|
id: "event-uuid-1",
|
||||||
slug: "deno-lab",
|
slug: "deno-lab",
|
||||||
@ -146,8 +127,6 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
|||||||
assertExists(cookies);
|
assertExists(cookies);
|
||||||
assert(cookies.includes(`session_id=${json.token};`));
|
assert(cookies.includes(`session_id=${json.token};`));
|
||||||
|
|
||||||
assert(selectCalled);
|
|
||||||
assert(updateCalled);
|
|
||||||
assert(auditCalled);
|
assert(auditCalled);
|
||||||
assertEquals(auditPayload.resource, "event-uuid-1");
|
assertEquals(auditPayload.resource, "event-uuid-1");
|
||||||
assertEquals(auditPayload.metadata.slug, "deno-lab");
|
assertEquals(auditPayload.metadata.slug, "deno-lab");
|
||||||
@ -160,186 +139,6 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await t.step(
|
|
||||||
"POST /api/join idempotent re-entry returns existing session without burning a seat",
|
|
||||||
async () => {
|
|
||||||
const valkeyGetStub = stub(valkey, "get", (key: any) => {
|
|
||||||
if (String(key) === "existing-guest-session") {
|
|
||||||
return Promise.resolve(
|
|
||||||
JSON.stringify({
|
|
||||||
uuid: "guest-uuid",
|
|
||||||
username: "guest_deno-lab_1",
|
|
||||||
sessionId: "existing-guest-session",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Promise.resolve(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
const originalSql = sqlWrapper.sql;
|
|
||||||
let updateCalled = false;
|
|
||||||
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
|
||||||
const query = Array.isArray(strings)
|
|
||||||
? strings.join("?")
|
|
||||||
: String(strings);
|
|
||||||
if (query.includes("SELECT * FROM event_passes")) {
|
|
||||||
return Promise.resolve([{
|
|
||||||
id: "event-uuid-1",
|
|
||||||
slug: "deno-lab",
|
|
||||||
pin_code: "749-123",
|
|
||||||
name: "Deno Workshop",
|
|
||||||
max_seats: 50,
|
|
||||||
seats_claimed: 1,
|
|
||||||
lifespan_hours: 3,
|
|
||||||
app_id: null,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
if (query.includes("UPDATE event_passes")) {
|
|
||||||
updateCalled = true;
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}) as any;
|
|
||||||
|
|
||||||
const valkeyExpireStub = stub(
|
|
||||||
valkey,
|
|
||||||
"expire",
|
|
||||||
() => Promise.resolve(1 as any),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await app.request("/api/join", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Forwarded-For": "192.168.1.1",
|
|
||||||
"Cookie": "session_id=existing-guest-session",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ code: "deno-lab" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
assertEquals(res.status, 200);
|
|
||||||
const json = await res.json();
|
|
||||||
assert(json.success === true);
|
|
||||||
assert(json.reused === true);
|
|
||||||
assertEquals(json.sessionId, "existing-guest-session");
|
|
||||||
assert(!updateCalled); // verify seat was not burned
|
|
||||||
} finally {
|
|
||||||
sqlWrapper.sql = originalSql;
|
|
||||||
valkeyGetStub.restore();
|
|
||||||
valkeyExpireStub.restore();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await t.step(
|
|
||||||
"POST /api/join enforces 5 failed attempts rate limit per IP",
|
|
||||||
async () => {
|
|
||||||
let isRateLimitedCalled = false;
|
|
||||||
let checkRateLimitCalled = false;
|
|
||||||
|
|
||||||
rateLimitWrapper.isRateLimited = (
|
|
||||||
key: string,
|
|
||||||
limit: number,
|
|
||||||
_window: number,
|
|
||||||
) => {
|
|
||||||
if (key === "ratelimit:join:fail:192.168.1.99" && limit === 5) {
|
|
||||||
isRateLimitedCalled = true;
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
|
||||||
return Promise.resolve(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
rateLimitWrapper.checkRateLimit = () => {
|
|
||||||
checkRateLimitCalled = true;
|
|
||||||
return Promise.resolve(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await app.request("/api/join", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Forwarded-For": "192.168.1.99",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ code: "invalid-code" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
assertEquals(res.status, 429);
|
|
||||||
const json = await res.json();
|
|
||||||
assertEquals(json.error, "Too Many Requests");
|
|
||||||
assert(isRateLimitedCalled);
|
|
||||||
assert(!checkRateLimitCalled); // didn't reach failure branch
|
|
||||||
} finally {
|
|
||||||
rateLimitWrapper.isRateLimited = () => Promise.resolve(false);
|
|
||||||
rateLimitWrapper.checkRateLimit = () => Promise.resolve(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await t.step(
|
|
||||||
"POST /api/join normalizes slug case-insensitively while preserving hyphens",
|
|
||||||
async () => {
|
|
||||||
const originalSql = sqlWrapper.sql;
|
|
||||||
sqlWrapper.sql = ((strings: any, ...values: any[]) => {
|
|
||||||
const query = Array.isArray(strings)
|
|
||||||
? strings.join("?")
|
|
||||||
: String(strings);
|
|
||||||
if (query.includes("SELECT * FROM event_passes")) {
|
|
||||||
assert(values.includes("deno-lab-workshop"));
|
|
||||||
return Promise.resolve([{
|
|
||||||
id: "event-uuid-2",
|
|
||||||
slug: "deno-lab-workshop",
|
|
||||||
pin_code: "111-222",
|
|
||||||
name: "Deno Workshop 2",
|
|
||||||
max_seats: 50,
|
|
||||||
seats_claimed: 0,
|
|
||||||
lifespan_hours: 3,
|
|
||||||
app_id: null,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
if (query.includes("UPDATE event_passes")) {
|
|
||||||
return Promise.resolve([{
|
|
||||||
id: "event-uuid-2",
|
|
||||||
slug: "deno-lab-workshop",
|
|
||||||
pin_code: "111-222",
|
|
||||||
name: "Deno Workshop 2",
|
|
||||||
max_seats: 50,
|
|
||||||
seats_claimed: 1,
|
|
||||||
lifespan_hours: 3,
|
|
||||||
app_id: null,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}) as any;
|
|
||||||
|
|
||||||
const valkeySetexStub = stub(
|
|
||||||
valkey,
|
|
||||||
"setex",
|
|
||||||
() => Promise.resolve("OK" as any),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await app.request("/api/join", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Forwarded-For": "192.168.1.1",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ code: " Deno-Lab-Workshop " }),
|
|
||||||
});
|
|
||||||
|
|
||||||
assertEquals(res.status, 200);
|
|
||||||
const json = await res.json();
|
|
||||||
assert(json.success === true);
|
|
||||||
assertEquals(json.username, "guest_deno-lab-workshop_1");
|
|
||||||
} finally {
|
|
||||||
sqlWrapper.sql = originalSql;
|
|
||||||
valkeySetexStub.restore();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await t.step(
|
await t.step(
|
||||||
"GET /join/:slug?format=env returns CLI export string",
|
"GET /join/:slug?format=env returns CLI export string",
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
@ -115,9 +115,7 @@ export const LoginPage = () => {
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div class="links">
|
<div class="links">
|
||||||
Don't have an account? <a href="/register">Register with Invite</a> |
|
Don't have an account? <a href="/register">Register with Invite</a>
|
||||||
{" "}
|
|
||||||
<a href="/join">Join with PIN</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -116,8 +116,7 @@ export const RegisterPage = (
|
|||||||
<div id="statusMessage"></div>
|
<div id="statusMessage"></div>
|
||||||
|
|
||||||
<div class="links">
|
<div class="links">
|
||||||
Already registered? <a href="/login">Sign in</a> |{" "}
|
Already registered? <a href="/login">Sign in</a>
|
||||||
<a href="/join">Join with PIN</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user