Skip to content
Merged
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
9 changes: 8 additions & 1 deletion .github/workflows/build-containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,21 @@ jobs:
echo "Pushed ${IMAGE_NAME}:latest"
fi

# 3. If dev branch, also push :dev
# 3. If dev branch, also push :dev and :beta
if [ "${BRANCH}" = "dev" ]; then
docker manifest rm ${IMAGE_NAME}:dev 2>/dev/null || true
docker manifest create ${IMAGE_NAME}:dev \
--amend ${IMAGE_NAME}:${VERSION}-amd64 \
--amend ${IMAGE_NAME}:${VERSION}-arm64
docker manifest push ${IMAGE_NAME}:dev
echo "Pushed ${IMAGE_NAME}:dev"

docker manifest rm ${IMAGE_NAME}:beta 2>/dev/null || true
docker manifest create ${IMAGE_NAME}:beta \
--amend ${IMAGE_NAME}:${VERSION}-amd64 \
--amend ${IMAGE_NAME}:${VERSION}-arm64
docker manifest push ${IMAGE_NAME}:beta
echo "Pushed ${IMAGE_NAME}:beta"
fi

- name: Verify Final Manifest
Expand Down
14 changes: 11 additions & 3 deletions apps/backend/src/api/routes/provision.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,24 @@ export class ProvisionController {
throw new HttpException('Invalid ticket type', HttpStatus.BAD_REQUEST);
}

// Atomic consume & replay protection: verify ticket exists in Redis then delete immediately
// Atomic consume & replay protection: verify ticket exists and delete in single atomic Redis transaction (Lua)
const ticketKey = `ticket:${payload.jti}`;
const storedTicket = await ioRedis.get(ticketKey);
const luaScript = `
local val = redis.call('GET', KEYS[1])
if val then
redis.call('DEL', KEYS[1])
return val
else
return nil
end
`;
const storedTicket = (await ioRedis.eval(luaScript, 1, ticketKey)) as string | null;
Comment on lines +206 to +215

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In environments where REDIS_URL is not defined (such as unit tests or local development), ioRedis is instantiated as MockRedis. Since MockRedis does not implement the eval method, calling ioRedis.eval will throw a runtime TypeError: ioRedis.eval is not a function.

To prevent this, we should check if eval is supported on the ioRedis instance, and fall back to the non-atomic get and del operations if it is not.

Suggested change
const luaScript = `
local val = redis.call('GET', KEYS[1])
if val then
redis.call('DEL', KEYS[1])
return val
else
return nil
end
`;
const storedTicket = (await ioRedis.eval(luaScript, 1, ticketKey)) as string | null;
const luaScript = "local val = redis.call('GET', KEYS[1]) if val then redis.call('DEL', KEYS[1]) return val else return nil end";
let storedTicket: string | null = null;
if (typeof ioRedis.eval === 'function') {
storedTicket = (await ioRedis.eval(luaScript, 1, ticketKey)) as string | null;
} else {
storedTicket = await ioRedis.get(ticketKey);
if (storedTicket) {
await ioRedis.del(ticketKey);
}
}

if (!storedTicket) {
throw new HttpException(
'Ticket has already been used or has expired',
HttpStatus.BAD_REQUEST
);
}
await ioRedis.del(ticketKey);

const user = await this._userService.getUserById(payload.userId);
if (!user || !user.activated) {
Expand Down
2 changes: 1 addition & 1 deletion scripts/docker-compose.beta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

services:
postiz-beta:
image: ghcr.io/gitroomhq/postiz-app:latest
image: ghcr.io/dos/crove-post:beta
container_name: postiz-beta
restart: always
env_file:
Expand Down
Loading