Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,878 changes: 1,878 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/database/autoddl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type ZodType,
type z,
} from "zod";

function toSqlType(type: ZodType): string {
const name = type.constructor.name;
if (name === ZodString.name) {
Expand Down
1 change: 1 addition & 0 deletions src/database/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
_condition_to_sql,
_select_columns_to_sql,
} from "sqlite-cloudflare-d1";

async function all(db: D1Database, query: string, values: Value[]) {
try {
const { results, success, error } = await db
Expand Down
76 changes: 71 additions & 5 deletions src/endpoints/freshmanAdd.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Bool, OpenAPIRoute } from "chanfana";
import { OpenAPIRoute } from "chanfana";
import type { Context } from "hono";
import { insert } from "sqlite-cloudflare-d1";
import { z } from "zod";
import type { Env } from "../../worker-configuration";
import { checkTable } from "../database";
import { JoinRequest } from "../types";
import { FreshmanRecord, JoinRequest } from "../types";

export class FreshmanAdd extends OpenAPIRoute {
schema = {
tags: ["Freshman"],
Expand All @@ -25,14 +26,46 @@ export class FreshmanAdd extends OpenAPIRoute {
"application/json": {
schema: z.object({
success: z.boolean(),
result: JoinRequest.optional(),
result: FreshmanRecord.optional(),
error: z.string().optional(),
requiresCaptcha: z.boolean().optional(),
}),
},
},
},
},
};

// Simple captcha validation (you can replace with more sophisticated validation)
private validateCaptcha(captcha: string): boolean {
// For demo purposes, we'll accept any 6-character string
// In production, this should validate against a proper captcha service
return captcha && captcha.length >= 6;
}

// Check if this is a duplicate submission based on email, phone, or student number
private async isDuplicateSubmission(
db: D1Database,
data: z.infer<typeof JoinRequest>,
): Promise<boolean> {
try {
const query = `
SELECT COUNT(*) as count
FROM freshman
WHERE email = ? OR phone = ? OR number = ?
`;
const result = await db
.prepare(query)
.bind(data.email, data.phone, data.number)
.first<{ count: number }>();

return (result?.count || 0) > 0;
} catch (error) {
console.error("Error checking duplicate submission:", error);
return false;
}
}

async handle(request: Context) {
// Get validated data
const data = await this.getValidatedData<typeof this.schema>();
Expand All @@ -41,12 +74,45 @@ export class FreshmanAdd extends OpenAPIRoute {
// Implement your own object insertion here
const env = request.env as Env;
const db = env.ACTIVE_DB as D1Database;

try {
await checkTable(db, "freshman", JoinRequest);
await checkTable(db, "freshman", FreshmanRecord);

// Check for duplicate submission
const isDuplicate = await this.isDuplicateSubmission(db, dataToCreate);

if (isDuplicate) {
// If duplicate and no captcha provided, require captcha
if (!dataToCreate.captcha) {
return {
success: false,
error: "重复提交检测到,请输入验证码",
requiresCaptcha: true,
};
}

// If duplicate and captcha provided, validate captcha
if (!this.validateCaptcha(dataToCreate.captcha)) {
return {
success: false,
error: "验证码无效,请重新输入",
requiresCaptcha: true,
};
}
}

// Prepare data for insertion (exclude captcha from database)
const { captcha: _captcha, ...dataForDB } = dataToCreate;
const recordToInsert = {
...dataForDB,
submissionTime: new Date(),
};

const row = await insert(db, {
into: "freshman",
data: dataToCreate,
data: recordToInsert,
});

return {
success: true,
result: row,
Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/freshmanFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class FreshmanFetch extends OpenAPIRoute {

const exists = true;

// @ts-ignore: check if the object exists
// @ts-expect-error: check if the object exists
if (exists === false) {
return Response.json(
{
Expand Down
25 changes: 14 additions & 11 deletions src/endpoints/freshmanList.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Bool, Num, OpenAPIRoute } from "chanfana";
import { Num, OpenAPIRoute } from "chanfana";
import { pageQuery } from "database";
import type { Context } from "hono";
import { z } from "zod";
import type { Env } from "../../worker-configuration";
import { JoinRequest } from "../types";
import { FreshmanRecord } from "../types";

export class FreshmanList extends OpenAPIRoute {
schema = {
Expand All @@ -23,7 +23,7 @@ export class FreshmanList extends OpenAPIRoute {
content: {
"application/json": {
schema: z.object({
list: z.array(JoinRequest),
list: z.array(FreshmanRecord),
total: z.number(),
}),
},
Expand Down Expand Up @@ -54,17 +54,20 @@ export class FreshmanList extends OpenAPIRoute {
...(page === undefined || page <= 0
? {}
: {
limit: pageSize,
offset: pageSize * (page - 1),
}),
limit: pageSize,
offset: pageSize * (page - 1),
}),
})) satisfies {
total: number;
}
};
} catch (error) {
return request.json({
error: error.message,
stacks: error.stack,
}, 500);
return request.json(
{
error: error.message,
stacks: error.stack,
},
500,
);
}
}
}
23 changes: 13 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@ const app = new Hono();
// Setup OpenAPI registry
app.use("*", async (c, next) => {
c.res.headers.set("Access-Control-Allow-Origin", "*");
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
c.res.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
c.res.headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
);
c.res.headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization",
);
if (c.req.method === "OPTIONS") {
return c.newResponse("OK",
200,
{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
}
);
return c.newResponse("OK", 200, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
});
}
await next();
});
Expand Down
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,18 @@ export const JoinRequest = z.object({
phone: Str({ example: "12345678901" }),
qq: Str({ example: "123456789" }),
memo: Str({ example: "lorem ipsum" }),
captcha: Str({ example: "abc123" }).optional(),
});

// Database schema that includes the submission time
export const FreshmanRecord = z.object({
name: z.string(),
number: z.string(),
major: z.string(),
class: z.string(),
email: z.string(),
phone: z.string(),
qq: z.string(),
memo: z.string(),
submissionTime: z.date(),
});