Skip to content

Commit bad82f8

Browse files
authored
feat: implement backend for auctions (#44)
2 parents 98b2b07 + de9df51 commit bad82f8

10 files changed

Lines changed: 312 additions & 15 deletions

File tree

backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import express, {
55
} from 'express';
66
import bodyParser from 'body-parser';
77
import usersRouter from './routes/users.ts';
8+
import auctionsRouter from './routes/auctions.ts';
89
import adminRouter from './routes/admin.ts';
910

1011
// Create the Express app.
@@ -32,6 +33,7 @@ app.use((req: Request, res: Response, next: NextFunction) => {
3233

3334
// Set up the endpoints for user management.
3435
app.use('/api/v1/users', usersRouter);
36+
app.use('/api/v1/auctions', auctionsRouter);
3537
app.use('/api/v1/admin', adminRouter);
3638

3739
// Add a default response for the root of the API.

backend/src/db/models/auction.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import mongoose, { Schema } from 'mongoose';
2+
import type { InferSchemaType } from 'mongoose';
3+
4+
// Create the DB schema for auctions.
5+
const auctionSchema = new Schema(
6+
{
7+
// Basic information about the auction.
8+
// All of this should be set when the auction is created.
9+
title: { type: String, required: true },
10+
description: { type: String, required: true },
11+
sellerId: { type: Schema.Types.ObjectId, ref: 'user', required: true },
12+
minimumBid: { type: Number, required: true, default: 0 },
13+
endDate: { type: Date, required: true, default: new Date() },
14+
15+
// This won't be filled in until the auction finishes.
16+
buyerId: { type: Schema.Types.ObjectId, ref: 'user' },
17+
18+
// These fields are used for the feedback system.
19+
expectedValue: { type: Number, default: 0 },
20+
},
21+
{
22+
timestamps: true,
23+
},
24+
);
25+
26+
export const Auction = mongoose.model('auction', auctionSchema);
27+
export type AuctionDataType = InferSchemaType<typeof auctionSchema>;

backend/src/db/models/user.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,25 @@ import mongoose, { Schema } from 'mongoose';
22
import type { InferSchemaType } from 'mongoose';
33

44
// Create the DB schema for users.
5-
const userSchema = new Schema({
6-
// No two accounts can have the same username, for obvious reasons.
7-
username: { type: String, required: true, unique: true },
8-
// The password will be encrypted.
9-
password: { type: String, required: true },
5+
const userSchema = new Schema(
6+
{
7+
// No two accounts can have the same username, for obvious reasons.
8+
username: { type: String, required: true, unique: true },
9+
// The password will be encrypted.
10+
password: { type: String, required: true },
1011

11-
//RBAC role for Admin user management such as locking accounts
12-
role: { type: String, enum: ['user', 'admin'], default: 'user' },
13-
locked: { type: Boolean, default: false },
14-
canBeLocked: { type: Boolean, default: true }, // Certain accounts cannot be locked
12+
//RBAC role for Admin user management such as locking accounts
13+
role: { type: String, enum: ['user', 'admin'], default: 'user' },
14+
locked: { type: Boolean, default: false },
15+
canBeLocked: { type: Boolean, default: true }, // Certain accounts cannot be locked
1516

16-
// "Tokens" are fake money in the platform.
17-
tokens: { type: Number, required: true, default: 0 },
18-
});
17+
// "Tokens" are fake money in the platform.
18+
tokens: { type: Number, required: true, default: 0 },
19+
},
20+
{
21+
timestamps: true,
22+
},
23+
);
1924

2025
export const User = mongoose.model('user', userSchema);
2126
export type UserDataType = InferSchemaType<typeof userSchema>;

backend/src/routes/auctions.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import express from 'express';
2+
import type { Request, Response } from 'express';
3+
import { requireAuth } from '../middleware/jwt.ts';
4+
import AuctionsService from '../services/auctions.ts';
5+
import {
6+
createAuctionSchema,
7+
updateAuctionSchema,
8+
} from 'treasure-trove-shared';
9+
10+
const auctionsRouter = express.Router();
11+
12+
// GET information about all auctions.
13+
auctionsRouter.get('/', requireAuth, async (_req: Request, res: Response) => {
14+
try {
15+
const auctions = await AuctionsService.getAllAuctions();
16+
res.status(200).json(auctions);
17+
} catch (error) {
18+
console.error('Error fetching auctions:', error);
19+
res.status(500).json({ error: 'Failed to fetch auctions' });
20+
}
21+
});
22+
23+
// GET information about the auction with the given ID.
24+
auctionsRouter.get('/:id', requireAuth, async (req: Request, res: Response) => {
25+
try {
26+
const auctionInfo = await AuctionsService.getAuctionById(req.params.id);
27+
return res.status(200).json(auctionInfo);
28+
} catch (error) {
29+
console.error('Error fetching auction info:', error);
30+
return res.status(404).json({ error: 'Auction not found' });
31+
}
32+
});
33+
34+
// POST endpoint to create a new auction.
35+
auctionsRouter.post('/', requireAuth, async (req: Request, res: Response) => {
36+
try {
37+
const validatedBody = createAuctionSchema.validateSync(req.body);
38+
const auction = AuctionsService.createAuction(validatedBody);
39+
// Use 201 when a POST request successfully creates a new resource on the server.
40+
return res.status(201).json(auction);
41+
} catch (error) {
42+
console.error('Auction creation error:', error);
43+
return res.status(400).json({
44+
error: 'Failed to create auction',
45+
});
46+
}
47+
});
48+
49+
// PUT endpoint to update information about an existing auction.
50+
auctionsRouter.put('/:id', requireAuth, async (req: Request, res: Response) => {
51+
try {
52+
const validatedBody = updateAuctionSchema.validateSync(req.body);
53+
const auctionInfo = await AuctionsService.updateAuction(
54+
req.params.id,
55+
validatedBody,
56+
);
57+
return res.status(200).json(auctionInfo);
58+
} catch (error) {
59+
console.error('Error updating auction:', error);
60+
// Use 400 when there is a bad request for some reason.
61+
return res.status(400).json({ error: 'failed to update auction ' });
62+
}
63+
});
64+
65+
export default auctionsRouter;

backend/src/services/auctions.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type {
2+
AuctionInfo,
3+
CreateAuctionInfo,
4+
UpdateAuctionInfo,
5+
} from '@shared/auctions.ts';
6+
import { Auction, type AuctionDataType } from '../db/models/auction.ts';
7+
8+
class AuctionsService {
9+
// Fetch information about all auctions in the database.
10+
static async getAllAuctions(): Promise<AuctionInfo[]> {
11+
const auctions = await Auction.find({});
12+
return auctions.map((a) =>
13+
AuctionsService.parseAuctionInfo(a._id.toString(), a),
14+
);
15+
}
16+
17+
// Fetch information about a particular auction.
18+
static async getAuctionById(auctionId: string): Promise<AuctionInfo> {
19+
const auction = await Auction.findById(auctionId);
20+
if (!auction) throw new Error('could not find auction!');
21+
return this.parseAuctionInfo(auctionId, auction);
22+
}
23+
24+
// Create a new auction.
25+
static async createAuction(
26+
auctionInfo: CreateAuctionInfo,
27+
): Promise<AuctionInfo> {
28+
const auction = new Auction({
29+
title: auctionInfo.title,
30+
description: auctionInfo.description,
31+
sellerId: auctionInfo.sellerId,
32+
minimumBid: auctionInfo.minimumBid,
33+
endDate: auctionInfo.endDate,
34+
expectedValue: auctionInfo.expectedValue,
35+
});
36+
await auction.save();
37+
return this.parseAuctionInfo(auction._id.toString(), auction);
38+
}
39+
40+
// Update information about an existing auction.
41+
static async updateAuction(
42+
auctionId: string,
43+
newAuction: Partial<UpdateAuctionInfo>,
44+
): Promise<AuctionInfo> {
45+
const auction = await Auction.findById(auctionId);
46+
if (!auction) throw new Error('could not update auction!');
47+
auction.set(newAuction);
48+
await auction.save();
49+
50+
return this.parseAuctionInfo(auctionId, auction);
51+
}
52+
53+
// This function is used for taking an auction DB document and converting it to
54+
// a usable interface.
55+
static parseAuctionInfo(
56+
auctionId: string,
57+
auction: AuctionDataType,
58+
): AuctionInfo {
59+
return {
60+
id: auctionId,
61+
title: auction.title,
62+
description: auction.description,
63+
sellerId: auction.sellerId.toString(),
64+
minimumBid: auction.minimumBid,
65+
endDate: auction.endDate,
66+
buyerId: auction.buyerId?.toString(),
67+
expectedValue: auction.expectedValue,
68+
createdDate: auction.createdAt,
69+
};
70+
}
71+
}
72+
73+
export default AuctionsService;

frontend/src/api/admin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { FullUserInfo } from '@shared/users.ts';
33
import { apiRoute, jwtHeaders } from './utils';
44

55
class AdminApi {
6+
// Retrieve information about all users.
67
static async getAllUsers(token: string): Promise<FullUserInfo[]> {
78
const res = await fetch(apiRoute('admin/users'), {
89
method: 'GET',
@@ -20,6 +21,8 @@ class AdminApi {
2021
return await res.json();
2122
}
2223

24+
// Lock out a user's account.
25+
// Locked users will not be able to log in.
2326
static async lockUser(id: string, token: string): Promise<void> {
2427
const res = await fetch(apiRoute(`admin/users/${id}/lock`), {
2528
method: 'POST',
@@ -35,6 +38,7 @@ class AdminApi {
3538
}
3639
}
3740

41+
// Unlock a user's account.
3842
static async unlockUser(id: string, token: string): Promise<void> {
3943
const res = await fetch(apiRoute(`admin/users/${id}/unlock`), {
4044
method: 'POST',

frontend/src/api/auctions.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/// <reference types="vite/client" />
2+
3+
import { apiRoute, jwtHeaders } from './utils';
4+
import {
5+
AuctionInfo,
6+
auctionInfoSchema,
7+
CreateAuctionInfo,
8+
UpdateAuctionInfo,
9+
} from '@shared/auctions.ts';
10+
11+
class AuctionsApi {
12+
// Retrieve information about all auctions.
13+
static async getAllAuctions(token: string): Promise<AuctionInfo[]> {
14+
const res = await fetch(apiRoute('auctions'), {
15+
method: 'GET',
16+
headers: {
17+
'Content-Type': 'application/json',
18+
...jwtHeaders(token),
19+
},
20+
});
21+
22+
if (!res.ok) throw new Error('failed to fetch auctions');
23+
24+
// Use the defined schema to validate the response.
25+
// This also takes care of converting the date strings to Date objects.
26+
const rawBody: AuctionInfo[] = await res.json();
27+
const body = rawBody.map((a) => auctionInfoSchema.validateSync(a));
28+
return body;
29+
}
30+
31+
// Retrieve information about a specific auction.
32+
static async getAuctionInfo(id: string, token: string): Promise<AuctionInfo> {
33+
const res = await fetch(apiRoute(`auctions/${id}`), {
34+
method: 'GET',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
...jwtHeaders(token),
38+
},
39+
});
40+
41+
if (!res.ok) throw new Error('failed to fetch auction');
42+
43+
// Use the defined schema to validate the response.
44+
// This also takes care of converting the date strings to a Date.
45+
const rawBody = await res.json();
46+
const body = auctionInfoSchema.validateSync(rawBody);
47+
return body;
48+
}
49+
50+
// Create a new auction.
51+
// If successful, the information about the new auction will be returned.
52+
static async createAuction(
53+
createAuctionInfo: CreateAuctionInfo,
54+
token: string,
55+
): Promise<AuctionInfo> {
56+
const res = await fetch(apiRoute('auctions'), {
57+
method: 'POST',
58+
headers: { 'Content-Type': 'application/json', ...jwtHeaders(token) },
59+
body: JSON.stringify(createAuctionInfo),
60+
});
61+
if (!res.ok) throw new Error('failed to create auction');
62+
const rawBody = await res.json();
63+
const body = auctionInfoSchema.validateSync(rawBody);
64+
return body;
65+
}
66+
67+
// Update information about an existing auction.
68+
// If successful, the auction's updated information will be returned.
69+
static async updateAuction(
70+
id: string,
71+
auctionInfo: Partial<UpdateAuctionInfo>,
72+
token: string,
73+
): Promise<AuctionInfo> {
74+
const res = await fetch(apiRoute(`auctions/${id}`), {
75+
method: 'PUT',
76+
headers: {
77+
'Content-Type': 'application/json',
78+
...jwtHeaders(token),
79+
},
80+
body: JSON.stringify(auctionInfo),
81+
});
82+
if (!res.ok) throw new Error('failed to update auction');
83+
const rawBody = await res.json();
84+
const body = auctionInfoSchema.validateSync(rawBody);
85+
return body;
86+
}
87+
}
88+
89+
export default AuctionsApi;

frontend/src/api/users.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@ import type {
88
} from '@shared/users.ts';
99
import { apiRoute, jwtHeaders } from './utils';
1010

11-
// This class defines frontend API methods for the backend database.
1211
class UserApi {
1312
// Register a new user account, which requires just a username and password.
1413
// If successful, the new user's info will be returned.
1514
static async signup(auth: UserCredentials): Promise<RegularUserInfo> {
16-
// Send request to create account.
1715
const res = await fetch(apiRoute('users/signup'), {
1816
method: 'POST',
1917
headers: { 'Content-Type': 'application/json' },
@@ -29,7 +27,6 @@ class UserApi {
2927
// Log in to an existing user account with a username and password.
3028
// If successful, the authentication info from the backend will be returned.
3129
static async login(auth: UserCredentials): Promise<AuthInfo> {
32-
// Send request to log in.
3330
const res = await fetch(apiRoute('users/login'), {
3431
method: 'POST',
3532
headers: { 'Content-Type': 'application/json' },
@@ -59,6 +56,7 @@ class UserApi {
5956
return await res.json();
6057
}
6158

59+
// Update information about an existing user.
6260
static async updateUser(
6361
id: string,
6462
user: Partial<FullUserInfo>,

shared/src/auctions.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as yup from 'yup';
2+
3+
// These are the only fields of an auction that can be updated after an auction is created.
4+
const updateAuctionSchema = yup.object({
5+
title: yup.string().required(),
6+
description: yup.string().required(),
7+
buyerId: yup.string(),
8+
});
9+
// Creating an auction requires the above fields plus these additional ones.
10+
// These fields cannot be updated after the auction is created.
11+
const createAuctionSchema = updateAuctionSchema.concat(
12+
yup.object({
13+
sellerId: yup.string().required(),
14+
minimumBid: yup.number().required().min(0),
15+
endDate: yup.date().required(),
16+
expectedValue: yup.number().min(0),
17+
}),
18+
);
19+
20+
// When the backend returns information about an auction, it includes
21+
// all of the above fields plus the auction ID.
22+
const auctionInfoSchema = createAuctionSchema.concat(
23+
yup.object({
24+
id: yup.string().required(),
25+
createdDate: yup.date().required(),
26+
}),
27+
);
28+
29+
export type UpdateAuctionInfo = yup.InferType<typeof updateAuctionSchema>;
30+
export type CreateAuctionInfo = yup.InferType<typeof createAuctionSchema>;
31+
export type AuctionInfo = yup.InferType<typeof auctionInfoSchema>;
32+
33+
export { updateAuctionSchema, createAuctionSchema, auctionInfoSchema };

0 commit comments

Comments
 (0)