Skip to content

Commit

Permalink
push before things become catastrophically bad
Browse files Browse the repository at this point in the history
  • Loading branch information
voxxal committed Jan 21, 2024
1 parent ebfe186 commit 5b103d9
Show file tree
Hide file tree
Showing 16 changed files with 174 additions and 55 deletions.
2 changes: 1 addition & 1 deletion migrations/1705422363022_add-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ exports.up = (pgm) => {
})

pgm.createTable('purchases', {
id: { type: 'uuid', primaryKey: true },
id: { type: 'uuid', primaryKey: true, default: 'gen_random_uuid()' },
type: { type: 'item_type', notNull: true },
itemid: {
type: 'string',
Expand Down
1 change: 0 additions & 1 deletion server/api/admin/store/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export default [
require('./list').default,
require('./get').default,
require('./put').default,
require('./delete').default
Expand Down
15 changes: 0 additions & 15 deletions server/api/admin/store/list.js

This file was deleted.

4 changes: 2 additions & 2 deletions server/api/challs/submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ export default {
await getChallengeInfo({ ids: [challengeid] })
)[0]
if (solves === 0) {
db.users.addChips(uuid, score + 150)
db.store.addChips(uuid, score + 150)
} else {
db.users.addChips(uuid, score)
db.store.addChips(uuid, score)
}
return responses.goodFlag
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion server/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const routes = [
...require('./integrations/client').default,
...require('./users').default,
...require('./auth').default,
...require('./admin').default
...require('./admin').default,
...require('./store').default
]

const makeSendResponse = (res) => (responseKind, data = null) => {
Expand Down
39 changes: 39 additions & 0 deletions server/api/store/buy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as db from '../../database'
import { responses } from '../../responses'

export default {
method: 'POST',
path: '/store/:id/buy',
requireAuth: true,
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string'
}
},
required: ['id']
}
},
handler: async ({ req, user }) => {
const item = await db.store.getItemById({ id: req.params.id })
const userObj = await db.users.getUserById({ id: user.id })
if (userObj.chips >= item.price) {
try {
return [
responses.goodPurchase,
await db.store.buyItem(userObj.id, item)
]
} catch (e) {
if (e.constraint === 'already_owned') {
return responses.badItemAlreadyOwned
}

throw e
}
}

return responses.badPurchase
}
}
4 changes: 4 additions & 0 deletions server/api/store/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default [
require('./buy').default,
require('./list').default
]
12 changes: 12 additions & 0 deletions server/api/store/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { responses } from '../../responses'
import * as store from '../../store'

export default {
method: 'GET',
path: '/store',
requireAuth: true,
handler: async () => {
const items = store.getAllItems()
return [responses.goodItems, items]
}
}
3 changes: 2 additions & 1 deletion server/api/users/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const getUserData = async ({ user }) => {
score: score.score,
globalPlace: score.globalPlace,
divisionPlace: score.divisionPlace,
solves
solves,
equippedItems: await db.store.getEquippedItems({ userid: user.id })
}
}
60 changes: 46 additions & 14 deletions server/cache/leaderboard.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { promisify } from 'util'
import client from './client'
import config from '../config/server'
import * as db from '../database'

const redisEvalsha = promisify(client.evalsha.bind(client))
const redisHget = promisify(client.hget.bind(client))
Expand All @@ -26,7 +27,9 @@ const luaChunkCall = `
end
`

const setLeaderboardScript = redisScript('load', `
const setLeaderboardScript = redisScript(
'load',
`
${luaChunkCall}
local leaderboard = cjson.decode(ARGV[1])
Expand Down Expand Up @@ -77,16 +80,22 @@ const setLeaderboardScript = redisScript('load', `
end
end
end
`)
`
)

const getRangeScript = redisScript('load', `
const getRangeScript = redisScript(
'load',
`
local result = redis.call("LRANGE", KEYS[1], ARGV[1], ARGV[2])
result[#result + 1] = redis.call("LLEN", KEYS[1])
return result
`)
`
)

// this script is not compatible with redis cluster as it computes key names at runtime
const getGraphScript = redisScript('load', `
const getGraphScript = redisScript(
'load',
`
local maxUsers = tonumber(ARGV[1])
local latest = redis.call("LRANGE", KEYS[1], 0, maxUsers * 3 - 1)
if #latest == 0 then
Expand All @@ -105,19 +114,28 @@ const getGraphScript = redisScript('load', `
latest,
users
})
`)
`
)

const setGraphScript = redisScript('load', `
const setGraphScript = redisScript(
'load',
`
${luaChunkCall}
redis.call("SET", KEYS[1], ARGV[1])
local users = cjson.decode(ARGV[2])
for i = 1, #users do
chunkCall("HSET", KEYS[i + 1], users[i])
end
`)
`
)

export const setLeaderboard = async ({ challengeValues, solveAmount, leaderboard, leaderboardUpdate }) => {
export const setLeaderboard = async ({
challengeValues,
solveAmount,
leaderboard,
leaderboardUpdate
}) => {
const divisions = Object.keys(config.divisions)
const divisionKeys = divisions.map(getLeaderboardKey)
const keys = [
Expand All @@ -131,6 +149,18 @@ export const setLeaderboard = async ({ challengeValues, solveAmount, leaderboard
challengeValues.forEach((value, key) => {
challengeInfo.push(key, `${value},${solveAmount.get(key)}`)
})
const lbWithItems = []
for (const info of leaderboard) {
const font = await db.store.getEquippedItemByType({
userid: info[0],
type: 'font'
})
lbWithItems.push([
...info,
JSON.stringify({ url: font?.resourceUrl, name: font?.resourceName })
])
}

await redisEvalsha(
await setLeaderboardScript,
keys.length,
Expand All @@ -157,7 +187,7 @@ export const getRange = async ({ start, end, division, all }) => {
if (!all && start === end) {
// zero-length query - get total only
return {
total: await redisLlen(getLeaderboardKey(division)) / 3,
total: (await redisLlen(getLeaderboardKey(division))) / 3,
leaderboard: []
}
}
Expand Down Expand Up @@ -265,10 +295,12 @@ export const getGraph = async ({ division, maxTeams }) => {
const graphData = parsed[2]
const result = []
for (let userIdx = 0; userIdx < latest.length / 3; userIdx++) {
const points = [{
time: lastUpdate,
score: parseInt(latest[userIdx * 3 + 2])
}]
const points = [
{
time: lastUpdate,
score: parseInt(latest[userIdx * 3 + 2])
}
]
const userPoints = graphData[userIdx]
for (let pointIdx = 0; pointIdx < userPoints.length; pointIdx += 2) {
points.push({
Expand Down
55 changes: 47 additions & 8 deletions server/database/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import db from './db'
import type { Item, Purchase } from '../store/types'
import { User } from './users'

export const getAllItems = (): Promise<Item[]> => {
return db
Expand Down Expand Up @@ -33,18 +34,30 @@ export const getPurchasesByUserId = ({
userid
}: Pick<Purchase, 'userid'>): Promise<Purchase[]> => {
return db
.query<Purchase[]>('SELECT * FROM purchases WHERE userid = $1', [userid])
.then((res) => res.rows[0])
.query<Purchase>('SELECT * FROM purchases WHERE userid = $1', [userid])
.then((res) => res.rows)
}

export const getEquippedItems = ({
userid
}: Pick<Purchase, 'userid'>): Promise<Purchase[]> => {
}: Pick<Purchase, 'userid'>): Promise<Item[]> => {
return db
.query<Purchase[]>(
'SELECT * FROM purchases WHERE userid = $1 AND equipped = 1',
.query<Item>(
'SELECT * FROM items WHERE id IN (SELECT itemid FROM purchases WHERE equipped = true AND userid = $1)',
[userid]
)
.then((res) => res.rows)
}

export const getEquippedItemByType = ({
type,
userid
}: Pick<Purchase, 'userid' | 'type'>): Promise<Item> => {
return db
.query<Item>(
'SELECT * FROM items WHERE id = (SELECT itemid FROM purchases WHERE equipped = true AND userid = $1 AND type = $2)',
[userid, type]
)
.then((res) => res.rows[0])
}

Expand All @@ -55,13 +68,13 @@ export const equipPurchase = async ({
}: Pick<Purchase, 'type' | 'userid' | 'itemid'>): Promise<Purchase> => {
// unequip all other items
await db.query(
'UPDATE purchases SET equipped = 0 WHERE userid = $1 AND type = $2',
'UPDATE purchases SET equipped = false WHERE userid = $1 AND type = $2',
[userid, type]
)

return db
.query<Purchase>(
'UPDATE purchases SET equipped = 1 WHERE userid = $1 AND itemid = $2 AND type = $3 RETURNING *',
'UPDATE purchases SET equipped = true WHERE userid = $1 AND itemid = $2 AND type = $3 RETURNING *',
[userid, itemid, type]
)
.then((res) => res.rows[0])
Expand All @@ -84,7 +97,6 @@ export const upsertItem = async ({
resourceUrl,
resourceName
}: Item): Promise<void> => {
console.log([id, type, price, name, description, resourceUrl, resourceName])
await db.query(
`INSERT INTO items VALUES($1, $2, $3, $4::item_type, $5, $6, $7)
ON CONFLICT (id)
Expand All @@ -99,3 +111,30 @@ export const upsertItem = async ({
[id, name, description, type, price, resourceUrl, resourceName]
)
}

export const addChips = (
id: string,
chips: number
): Promise<User | undefined> => {
return db
.query<User>('UPDATE users SET chips = chips + $1 WHERE id = $2', [
chips,
id
])
.then((res) => res.rows[0])
}

export const buyItem = async (
userid: string,
item: Item
): Promise<Purchase> => {
const purchase = await db
.query<Purchase>(
'INSERT INTO purchases (type, itemid, userid) VALUES ($1, $2, $3) RETURNING *',
[item.type, item.id, userid]
)
.then((res) => res.rows[0])
// is this, timing attack?
await addChips(userid, -item.price)
return purchase
}
5 changes: 0 additions & 5 deletions server/database/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ export const getUserById = ({ id }: Pick<User, 'id'>): Promise<User | undefined>
.then(res => res.rows[0])
}

export const addChips = (id: string, chips: number): Promise<User | undefined> => {
return db.query<User>('UPDATE users SET chips = chips + $1 WHERE id = $2', [chips, id])
.then(res => res.rows[0])
}

export const getUserByEmail = ({ email }: Pick<User, 'email'>): Promise<User | undefined> => {
return db.query<User>('SELECT * FROM users WHERE email = $1', [email])
.then(res => res.rows[0])
Expand Down
10 changes: 5 additions & 5 deletions server/providers/store/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ class DatabaseProvider extends EventEmitter implements Provider {
private items: Item[] = []
private purchases: Purchase[] = []

constructor() {
constructor () {
super()
void this.update()
}

private async update(): Promise<void> {
private async update (): Promise<void> {
try {
const dbchallenges = await db.store.getAllItems()

Expand All @@ -27,11 +27,11 @@ class DatabaseProvider extends EventEmitter implements Provider {
}
}

forceUpdate(): void {
forceUpdate (): void {
void this.update()
}

async updateItem(item: Item): Promise<void> {
async updateItem (item: Item): Promise<void> {
const originalData = await db.store.getItemById({
id: item.id
})
Expand All @@ -51,7 +51,7 @@ class DatabaseProvider extends EventEmitter implements Provider {
void this.update()
}

async deleteItem(id: string): Promise<void> {
async deleteItem (id: string): Promise<void> {
await db.store.removeItemById({ id: id })

void this.update()
Expand Down
Loading

0 comments on commit 5b103d9

Please sign in to comment.