diff --git a/modules/auth/module.json b/modules/auth/module.json index 0b7d940f..165c2099 100644 --- a/modules/auth/module.json +++ b/modules/auth/module.json @@ -1,52 +1,52 @@ { - "name": "Authentication", - "description": "Authenticate users with multiple authentication methods.", - "icon": "key", - "tags": [ - "core", - "auth", - "user" - ], - "authors": [ - "rivet-gg", - "NathanFlurry" - ], - "status": "stable", - "dependencies": { - "email": {}, - "users": {}, - "rate_limit": {} - }, - "scripts": { - "send_email_verification": { - "name": "Send Email Verification", - "description": "Send a one-time verification code to a user's email address to authenticate them.", - "public": true - }, - "complete_email_verification": { - "name": "Complete Email Verification", - "description": "Verify a user's email address with a one-time verification code.", - "public": true - } - }, - "errors": { - "provider_disabled": { - "name": "Provider Disabled" - }, - "verification_code_invalid": { - "name": "Verification Code Invalid" - }, - "verification_code_attempt_limit": { - "name": "Verification Code Attempt Limit" - }, - "verification_code_expired": { - "name": "Verification Code Expired" - }, - "verification_code_already_used": { - "name": "Verification Code Already Used" - }, - "email_already_used": { - "name": "Email Already Used" - } - } + "name": "Authentication", + "description": "Authenticate users with multiple authentication methods.", + "icon": "key", + "tags": [ + "core", + "auth", + "user" + ], + "authors": [ + "rivet-gg", + "NathanFlurry" + ], + "status": "stable", + "dependencies": { + "email": {}, + "users": {}, + "rate_limit": {} + }, + "scripts": { + "send_email_verification": { + "name": "Send Email Verification", + "description": "Send a one-time verification code to a user's email address to authenticate them.", + "public": true + }, + "complete_email_verification": { + "name": "Complete Email Verification", + "description": "Verify a user's email address with a one-time verification code.", + "public": true + } + }, + "errors": { + "provider_disabled": { + "name": "Provider Disabled" + }, + "verification_code_invalid": { + "name": "Verification Code Invalid" + }, + "verification_code_attempt_limit": { + "name": "Verification Code Attempt Limit" + }, + "verification_code_expired": { + "name": "Verification Code Expired" + }, + "verification_code_already_used": { + "name": "Verification Code Already Used" + }, + "email_already_used": { + "name": "Email Already Used" + } + } } diff --git a/modules/auth/scripts/complete_email_verification.ts b/modules/auth/scripts/complete_email_verification.ts index 684914b7..c10c6f45 100644 --- a/modules/auth/scripts/complete_email_verification.ts +++ b/modules/auth/scripts/complete_email_verification.ts @@ -1,8 +1,5 @@ import { assertExists } from "https://deno.land/std@0.208.0/assert/mod.ts"; -import { - RuntimeError, - ScriptContext, -} from "../module.gen.ts"; +import { RuntimeError, ScriptContext } from "../module.gen.ts"; import { TokenWithSecret } from "../../tokens/utils/types.ts"; export interface Request { diff --git a/modules/auth/tests/e2e.ts b/modules/auth/tests/e2e.ts index 7242c6b7..5c3b1bc3 100644 --- a/modules/auth/tests/e2e.ts +++ b/modules/auth/tests/e2e.ts @@ -9,7 +9,7 @@ test("e2e", async (ctx: TestContext) => { const { user } = await ctx.modules.users.create({}); const { token: session } = await ctx.modules.users.createToken({ - userId: user.id + userId: user.id, }); const fakeEmail = faker.internet.email(); @@ -18,15 +18,16 @@ test("e2e", async (ctx: TestContext) => { { const authRes = await ctx.modules.auth.sendEmailVerification({ email: fakeEmail, - userToken: session.token + userToken: session.token, }); // Look up correct code - const { code } = await ctx.db.emailPasswordlessVerification.findFirstOrThrow({ - where: { - id: authRes.verification.id, - }, - }); + const { code } = await ctx.db.emailPasswordlessVerification + .findFirstOrThrow({ + where: { + id: authRes.verification.id, + }, + }); // Now by verifying the email, we register, and can also use // this to verify the token @@ -37,10 +38,9 @@ test("e2e", async (ctx: TestContext) => { assertEquals(verifyRes.token.type, "user"); - // Make sure we end up with the same user we started with const verifyRes2 = await ctx.modules.users.authenticateToken({ - userToken: verifyRes.token.token + userToken: verifyRes.token.token, }); assertEquals(verifyRes2.userId, user.id); @@ -50,15 +50,16 @@ test("e2e", async (ctx: TestContext) => { // but without a token, expecting the same user { const authRes = await ctx.modules.auth.sendEmailVerification({ - email: fakeEmail + email: fakeEmail, }); // Look up correct code - const { code: code } = await ctx.db.emailPasswordlessVerification.findFirstOrThrow({ - where: { - id: authRes.verification.id, - }, - }); + const { code: code } = await ctx.db.emailPasswordlessVerification + .findFirstOrThrow({ + where: { + id: authRes.verification.id, + }, + }); const verifyRes = await ctx.modules.auth.completeEmailVerification({ verificationId: authRes.verification.id, @@ -66,10 +67,9 @@ test("e2e", async (ctx: TestContext) => { }); const verifyRes2 = await ctx.modules.users.authenticateToken({ - userToken: verifyRes.token.token + userToken: verifyRes.token.token, }); - assertEquals(verifyRes2.userId, user.id); + assertEquals(verifyRes2.userId, user.id); } }); - diff --git a/modules/currency/module.json b/modules/currency/module.json index ed9641ff..5ac32177 100644 --- a/modules/currency/module.json +++ b/modules/currency/module.json @@ -1,45 +1,45 @@ { - "name": "Currency", - "description": "Track user balances and allow them to deposit and withdraw.", - "icon": "coin", - "tags": [ - "economy" - ], - "authors": [ - "ABCxFF" - ], - "status": "preview", - "dependencies": { - "rate_limit": {}, - "users": {} - }, - "scripts": { - "deposit": { - "name": "Deposit" - }, - "withdraw": { - "name": "Withdraw" - }, - "get_balance": { - "name": "Get Balance" - }, - "set_balance": { - "name": "Set Balance" - }, - "get_balance_by_token": { - "name": "Get Balance by Token", - "public": true - } - }, - "errors": { - "invalid_user": { - "name": "Invalid User" - }, - "invalid_amount": { - "name": "Invalid Amount" - }, - "not_enough_funds": { - "name": "Not Enough Funds" - } - } -} \ No newline at end of file + "name": "Currency", + "description": "Track user balances and allow them to deposit and withdraw.", + "icon": "coin", + "tags": [ + "economy" + ], + "authors": [ + "ABCxFF" + ], + "status": "preview", + "dependencies": { + "rate_limit": {}, + "users": {} + }, + "scripts": { + "deposit": { + "name": "Deposit" + }, + "withdraw": { + "name": "Withdraw" + }, + "get_balance": { + "name": "Get Balance" + }, + "set_balance": { + "name": "Set Balance" + }, + "get_balance_by_token": { + "name": "Get Balance by Token", + "public": true + } + }, + "errors": { + "invalid_user": { + "name": "Invalid User" + }, + "invalid_amount": { + "name": "Invalid Amount" + }, + "not_enough_funds": { + "name": "Not Enough Funds" + } + } +} diff --git a/modules/email/module.json b/modules/email/module.json index 374f1d84..14396f34 100644 --- a/modules/email/module.json +++ b/modules/email/module.json @@ -1,27 +1,27 @@ { - "name": "Email", - "description": "Send emails using multiple providers.", - "icon": "envelope", - "tags": [ - "email" - ], - "authors": [ - "rivet-gg", - "NathanFlurry" - ], - "status": "stable", - "scripts": { - "send_email": { - "name": "Send Email" - } - }, - "errors": { - "email_missing_content": { - "name": "Email Missing Content", - "description": "Email must have `html` and/or `text`" - }, - "sendgrid_error": { - "name": "SendGrid Error" - } - } -} \ No newline at end of file + "name": "Email", + "description": "Send emails using multiple providers.", + "icon": "envelope", + "tags": [ + "email" + ], + "authors": [ + "rivet-gg", + "NathanFlurry" + ], + "status": "stable", + "scripts": { + "send_email": { + "name": "Send Email" + } + }, + "errors": { + "email_missing_content": { + "name": "Email Missing Content", + "description": "Email must have `html` and/or `text`" + }, + "sendgrid_error": { + "name": "SendGrid Error" + } + } +} diff --git a/modules/email/scripts/send_email.ts b/modules/email/scripts/send_email.ts index f6d78afd..ab717c91 100644 --- a/modules/email/scripts/send_email.ts +++ b/modules/email/scripts/send_email.ts @@ -37,9 +37,9 @@ export async function run( } async function useSendGrid(config: ProviderSendGrid, req: Request) { - const apiKeyVariable = config.apiKeyVariable ?? "SENDGRID_API_KEY"; - const apiKey = Deno.env.get(apiKeyVariable); - assertExists(apiKey, `Missing environment variable: ${apiKeyVariable}`); + const apiKeyVariable = config.apiKeyVariable ?? "SENDGRID_API_KEY"; + const apiKey = Deno.env.get(apiKeyVariable); + assertExists(apiKey, `Missing environment variable: ${apiKeyVariable}`); const content = []; if (req.text) { diff --git a/modules/friends/module.json b/modules/friends/module.json index a6bb3f08..19cd6e9f 100644 --- a/modules/friends/module.json +++ b/modules/friends/module.json @@ -1,77 +1,77 @@ { - "name": "Friends", - "description": "Allow users to send and accept friend requests.", - "icon": "user-group", - "tags": [ - "social" - ], - "authors": [ - "rivet-gg", - "NathanFlurry" - ], - "status": "beta", - "dependencies": { - "rate_limit": {}, - "users": {} - }, - "scripts": { - "send_request": { - "name": "Send Request", - "description": "Send a friend request to another user.", - "public": true - }, - "accept_request": { - "name": "Accept Request", - "description": "Accept a friend request from another user.", - "public": true - }, - "decline_request": { - "name": "Decline Request", - "description": "Decline a friend request from another user.", - "public": true - }, - "remove_friend": { - "name": "Remove Friend", - "description": "Remove a friend from your friends list.", - "public": true - }, - "list_friends": { - "name": "List Friends", - "description": "List all friends of a user.", - "public": true - }, - "list_outgoing_friend_requests": { - "name": "List Outgoing Friend Requests", - "description": "List all friend requests sent by a user.", - "public": true - }, - "list_incoming_friend_requests": { - "name": "List Incoming Friend Requests", - "description": "List all friend requests received by a user.", - "public": true - } - }, - "errors": { - "already_friends": { - "name": "Already Friends" - }, - "friend_request_not_found": { - "name": "Friend Request Not Found" - }, - "friend_request_already_exists": { - "name": "Friend Request Already Exists" - }, - "not_friend_request_recipient": { - "name": "Not Friend Request Recipient" - }, - "friend_request_already_accepted": { - "name": "Friend Request Already Accepted" - }, - "friend_request_already_declined": { - "name": "Friend Request Already Declined" - }, - "cannot_send_to_self": { - "name": "Cannot Send to Self" - } - } -} \ No newline at end of file + "name": "Friends", + "description": "Allow users to send and accept friend requests.", + "icon": "user-group", + "tags": [ + "social" + ], + "authors": [ + "rivet-gg", + "NathanFlurry" + ], + "status": "beta", + "dependencies": { + "rate_limit": {}, + "users": {} + }, + "scripts": { + "send_request": { + "name": "Send Request", + "description": "Send a friend request to another user.", + "public": true + }, + "accept_request": { + "name": "Accept Request", + "description": "Accept a friend request from another user.", + "public": true + }, + "decline_request": { + "name": "Decline Request", + "description": "Decline a friend request from another user.", + "public": true + }, + "remove_friend": { + "name": "Remove Friend", + "description": "Remove a friend from your friends list.", + "public": true + }, + "list_friends": { + "name": "List Friends", + "description": "List all friends of a user.", + "public": true + }, + "list_outgoing_friend_requests": { + "name": "List Outgoing Friend Requests", + "description": "List all friend requests sent by a user.", + "public": true + }, + "list_incoming_friend_requests": { + "name": "List Incoming Friend Requests", + "description": "List all friend requests received by a user.", + "public": true + } + }, + "errors": { + "already_friends": { + "name": "Already Friends" + }, + "friend_request_not_found": { + "name": "Friend Request Not Found" + }, + "friend_request_already_exists": { + "name": "Friend Request Already Exists" + }, + "not_friend_request_recipient": { + "name": "Not Friend Request Recipient" + }, + "friend_request_already_accepted": { + "name": "Friend Request Already Accepted" + }, + "friend_request_already_declined": { + "name": "Friend Request Already Declined" + }, + "cannot_send_to_self": { + "name": "Cannot Send to Self" + } + } +} diff --git a/modules/friends/scripts/accept_request.ts b/modules/friends/scripts/accept_request.ts index bfc7b000..a852dd39 100644 --- a/modules/friends/scripts/accept_request.ts +++ b/modules/friends/scripts/accept_request.ts @@ -26,14 +26,14 @@ export async function run( declinedAt: Date | null; } const friendRequests = await tx.$queryRawUnsafe( - ` + ` SELECT "senderUserId", "targetUserId", "acceptedAt", "declinedAt" FROM "${ctx.dbSchema}"."FriendRequest" WHERE "id" = $1 FOR UPDATE `, - req.friendRequestId - ); + req.friendRequestId, + ); const friendRequest = friendRequests[0]; if (!friendRequest) { throw new RuntimeError("FRIEND_REQUEST_NOT_FOUND", { diff --git a/modules/friends/scripts/decline_request.ts b/modules/friends/scripts/decline_request.ts index e30589ee..bb22ea54 100644 --- a/modules/friends/scripts/decline_request.ts +++ b/modules/friends/scripts/decline_request.ts @@ -1,7 +1,4 @@ -import { - RuntimeError, - ScriptContext, -} from "../module.gen.ts"; +import { RuntimeError, ScriptContext } from "../module.gen.ts"; export interface Request { userToken: string; @@ -28,15 +25,15 @@ export async function run( acceptedAt: Date | null; declinedAt: Date | null; } - const friendRequests = await tx.$queryRawUnsafe( - ` + const friendRequests = await tx.$queryRawUnsafe( + ` SELECT "senderUserId", "targetUserId", "acceptedAt", "declinedAt" FROM "${ctx.dbSchema}"."FriendRequest" WHERE "id" = $1 FOR UPDATE `, - req.friendRequestId - ); + req.friendRequestId, + ); const friendRequest = friendRequests[0]; if (!friendRequest) { throw new RuntimeError("FRIEND_REQUEST_NOT_FOUND", { diff --git a/modules/friends/scripts/send_request.ts b/modules/friends/scripts/send_request.ts index e0aaa63b..0ba41839 100644 --- a/modules/friends/scripts/send_request.ts +++ b/modules/friends/scripts/send_request.ts @@ -31,15 +31,15 @@ export async function run( // Validate that the users are not already friends // TODO: Remove this `any` and replace with a proper type const existingFriendRows = await tx.$queryRawUnsafe( - ` + ` SELECT 1 FROM "${ctx.dbSchema}"."Friend" WHERE "userIdA" = $1 OR "userIdB" = $2 FOR UPDATE `, - userIdA, - userIdB, - ); + userIdA, + userIdB, + ); if (existingFriendRows.length > 0) { throw new RuntimeError("ALREADY_FRIENDS", { meta: { userIdA, userIdB } }); } diff --git a/modules/friends/tests/e2e.ts b/modules/friends/tests/e2e.ts index 22f30a53..b84ce4cb 100644 --- a/modules/friends/tests/e2e.ts +++ b/modules/friends/tests/e2e.ts @@ -6,12 +6,16 @@ test("e2e accept", async (ctx: TestContext) => { const { user: userA } = await ctx.modules.users.create({ username: faker.internet.userName(), }); - const { token: tokenA } = await ctx.modules.users.createToken({ userId: userA.id }); + const { token: tokenA } = await ctx.modules.users.createToken({ + userId: userA.id, + }); const { user: userB } = await ctx.modules.users.create({ username: faker.internet.userName(), }); - const { token: tokenB } = await ctx.modules.users.createToken({ userId: userB.id }); + const { token: tokenB } = await ctx.modules.users.createToken({ + userId: userB.id, + }); const { friendRequest } = await ctx.modules.friends.sendRequest({ userToken: tokenA.token, @@ -60,12 +64,16 @@ test("e2e reject", async (ctx: TestContext) => { const { user: userA } = await ctx.modules.users.create({ username: faker.internet.userName(), }); - const { token: tokenA } = await ctx.modules.users.createToken({ userId: userA.id }); + const { token: tokenA } = await ctx.modules.users.createToken({ + userId: userA.id, + }); const { user: userB } = await ctx.modules.users.create({ username: faker.internet.userName(), }); - const { token: tokenB } = await ctx.modules.users.createToken({ userId: userB.id }); + const { token: tokenB } = await ctx.modules.users.createToken({ + userId: userB.id, + }); const { friendRequest } = await ctx.modules.friends.sendRequest({ userToken: tokenA.token, diff --git a/modules/rate_limit/actors/limiter.ts b/modules/rate_limit/actors/limiter.ts index aea0f643..1ce7ebd2 100644 --- a/modules/rate_limit/actors/limiter.ts +++ b/modules/rate_limit/actors/limiter.ts @@ -3,44 +3,46 @@ import { ActorBase, ActorContext } from "../module.gen.ts"; type Input = undefined; interface State { - tokens: number; - lastRefillTimestamp: number; + tokens: number; + lastRefillTimestamp: number; } export interface ThrottleRequest { - requests: number; - period: number; + requests: number; + period: number; } export interface ThrottleResponse { - success: boolean; - refillAt: number; + success: boolean; + refillAt: number; } export class Actor extends ActorBase { - public initialize(): State { - // Will refill on first call of `throttle` - return { - tokens: 0, - lastRefillTimestamp: 0, - }; - } - - throttle(_ctx: ActorContext, req: ThrottleRequest): ThrottleResponse { - // Reset bucket - const now = Date.now(); - if (now > this.state.lastRefillTimestamp + req.period * 1000) { - this.state.tokens = req.requests; - this.state.lastRefillTimestamp = now; - } - - // Attempt to consume token - const success = this.state.tokens >= 1; - if (success) { - this.state.tokens -= 1; - } - - const refillAt = Math.ceil((1 - this.state.tokens) * (req.period / req.requests)); - return { success, refillAt }; - } + public initialize(): State { + // Will refill on first call of `throttle` + return { + tokens: 0, + lastRefillTimestamp: 0, + }; + } + + throttle(_ctx: ActorContext, req: ThrottleRequest): ThrottleResponse { + // Reset bucket + const now = Date.now(); + if (now > this.state.lastRefillTimestamp + req.period * 1000) { + this.state.tokens = req.requests; + this.state.lastRefillTimestamp = now; + } + + // Attempt to consume token + const success = this.state.tokens >= 1; + if (success) { + this.state.tokens -= 1; + } + + const refillAt = Math.ceil( + (1 - this.state.tokens) * (req.period / req.requests), + ); + return { success, refillAt }; + } } diff --git a/modules/rate_limit/module.json b/modules/rate_limit/module.json index 3210575e..5542b31e 100644 --- a/modules/rate_limit/module.json +++ b/modules/rate_limit/module.json @@ -29,4 +29,4 @@ "actors": { "limiter": {} } -} \ No newline at end of file +} diff --git a/modules/rate_limit/scripts/throttle.ts b/modules/rate_limit/scripts/throttle.ts index fa36cac3..a186ac3e 100644 --- a/modules/rate_limit/scripts/throttle.ts +++ b/modules/rate_limit/scripts/throttle.ts @@ -30,19 +30,23 @@ export async function run( ctx: ScriptContext, req: Request, ): Promise { - assert(req.requests > 0); - assert(req.period > 0); - - // Create key - const key = `${JSON.stringify(req.type)}.${JSON.stringify(req.key)}`; - - // Throttle request - const res = await ctx.actors.limiter.getOrCreateAndCall(key, undefined, "throttle", { - requests: req.requests, - period: req.period, - }); - - // Check if allowed + assert(req.requests > 0); + assert(req.period > 0); + + // Create key + const key = `${JSON.stringify(req.type)}.${JSON.stringify(req.key)}`; + + // Throttle request + const res = await ctx.actors.limiter.getOrCreateAndCall< + undefined, + ThrottleRequest, + ThrottleResponse + >(key, undefined, "throttle", { + requests: req.requests, + period: req.period, + }); + + // Check if allowed if (!res.success) { throw new RuntimeError("RATE_LIMIT_EXCEEDED", { meta: { diff --git a/modules/rate_limit/scripts/throttle_public.ts b/modules/rate_limit/scripts/throttle_public.ts index d3e4356f..1422432e 100644 --- a/modules/rate_limit/scripts/throttle_public.ts +++ b/modules/rate_limit/scripts/throttle_public.ts @@ -29,8 +29,8 @@ export async function run( } } - // If no IP address, this request is not coming from a client and should not - // be throttled + // If no IP address, this request is not coming from a client and should not + // be throttled if (!key) { return {}; } diff --git a/modules/rate_limit/tests/e2e.ts b/modules/rate_limit/tests/e2e.ts index 1ecb6621..f922dce5 100644 --- a/modules/rate_limit/tests/e2e.ts +++ b/modules/rate_limit/tests/e2e.ts @@ -31,9 +31,9 @@ test("e2e", async (ctx: TestContext) => { assertEquals("RATE_LIMIT_EXCEEDED", error.code); } - // Wait for the rate limit to reset - await delay(period * 1000); + // Wait for the rate limit to reset + await delay(period * 1000); - // Should be able to make requests again - await makeRequest(); + // Should be able to make requests again + await makeRequest(); }); diff --git a/modules/tokens/module.json b/modules/tokens/module.json index 2cae135f..6c9853dc 100644 --- a/modules/tokens/module.json +++ b/modules/tokens/module.json @@ -1,50 +1,50 @@ { - "name": "Tokens", - "description": "Create & verify tokens for authorization purposes.", - "icon": "lock", - "tags": [ - "core", - "utility" - ], - "authors": [ - "rivet-gg", - "NathanFlurry" - ], - "status": "stable", - "scripts": { - "create": { - "name": "Create Token" - }, - "fetch": { - "name": "Fetch Token", - "description": "Get a token by its ID." - }, - "fetch_by_token": { - "name": "Fetch by Token", - "description": "Get a token by its secret token." - }, - "revoke": { - "name": "Revoke Token", - "description": "Revoke a token, preventing it from being used again." - }, - "validate": { - "name": "Validate Token", - "description": "Validate a token. Throws an error if the token is invalid." - }, - "extend": { - "name": "Extend Token", - "description": "Extend or remove the expiration date of a token. (Only works on valid tokens.)" - } - }, - "errors": { - "token_not_found": { - "name": "Token Not Found" - }, - "token_revoked": { - "name": "Token Revoked" - }, - "token_expired": { - "name": "Token Expired" - } - } + "name": "Tokens", + "description": "Create & verify tokens for authorization purposes.", + "icon": "lock", + "tags": [ + "core", + "utility" + ], + "authors": [ + "rivet-gg", + "NathanFlurry" + ], + "status": "stable", + "scripts": { + "create": { + "name": "Create Token" + }, + "fetch": { + "name": "Fetch Token", + "description": "Get a token by its ID." + }, + "fetch_by_token": { + "name": "Fetch by Token", + "description": "Get a token by its secret token." + }, + "revoke": { + "name": "Revoke Token", + "description": "Revoke a token, preventing it from being used again." + }, + "validate": { + "name": "Validate Token", + "description": "Validate a token. Throws an error if the token is invalid." + }, + "extend": { + "name": "Extend Token", + "description": "Extend or remove the expiration date of a token. (Only works on valid tokens.)" + } + }, + "errors": { + "token_not_found": { + "name": "Token Not Found" + }, + "token_revoked": { + "name": "Token Revoked" + }, + "token_expired": { + "name": "Token Expired" + } + } } diff --git a/modules/tokens/scripts/extend.ts b/modules/tokens/scripts/extend.ts index 35fe2195..5247d473 100644 --- a/modules/tokens/scripts/extend.ts +++ b/modules/tokens/scripts/extend.ts @@ -1,9 +1,9 @@ import { ScriptContext } from "../module.gen.ts"; -import { TokenWithSecret, tokenFromRow } from "../utils/types.ts"; +import { tokenFromRow, TokenWithSecret } from "../utils/types.ts"; export interface Request { - token: string; - newExpiration: string | null; + token: string; + newExpiration: string | null; } export interface Response { @@ -14,22 +14,22 @@ export async function run( ctx: ScriptContext, req: Request, ): Promise { - // Ensure the token hasn't expired or been revoked yet - const { token } = await ctx.modules.tokens.validate({ - token: req.token, - }); + // Ensure the token hasn't expired or been revoked yet + const { token } = await ctx.modules.tokens.validate({ + token: req.token, + }); - // Update the token's expiration date - const newToken = await ctx.db.token.update({ - where: { - id: token.id, - }, - data: { - expireAt: req.newExpiration, - }, - }); + // Update the token's expiration date + const newToken = await ctx.db.token.update({ + where: { + id: token.id, + }, + data: { + expireAt: req.newExpiration, + }, + }); - // Return the updated token + // Return the updated token return { token: tokenFromRow(newToken), }; diff --git a/modules/tokens/scripts/revoke.ts b/modules/tokens/scripts/revoke.ts index ac0d8662..18d2a1c5 100644 --- a/modules/tokens/scripts/revoke.ts +++ b/modules/tokens/scripts/revoke.ts @@ -26,7 +26,7 @@ export async function run( // Sets revokedAt on all tokens that have not already been revoked. Returns // wether or not each token was revoked. const rows = await ctx.db.$queryRawUnsafe( - ` + ` WITH "PreUpdate" AS ( SELECT "id", "revokedAt" FROM "${ctx.dbSchema}"."Token" @@ -38,8 +38,8 @@ export async function run( WHERE "Token"."id" = "PreUpdate"."id" RETURNING "Token"."id" AS "id", "PreUpdate"."revokedAt" IS NOT NULL AS "alreadyRevoked" `, - req.tokenIds, - ); + req.tokenIds, + ); const updates: Record = {}; for (const tokenId of req.tokenIds) { diff --git a/modules/tokens/tests/validate.ts b/modules/tokens/tests/validate.ts index 13a07a39..cbcecbe6 100644 --- a/modules/tokens/tests/validate.ts +++ b/modules/tokens/tests/validate.ts @@ -1,8 +1,8 @@ import { RuntimeError, test, TestContext } from "../module.gen.ts"; import { assertEquals, - assertRejects, assertGreater, + assertRejects, } from "https://deno.land/std@0.217.0/assert/mod.ts"; test( @@ -97,6 +97,6 @@ test( }, { ...token, expireAt: null, - }) + }); }, ); diff --git a/modules/uploads/module.json b/modules/uploads/module.json index 4a6487df..2f761509 100644 --- a/modules/uploads/module.json +++ b/modules/uploads/module.json @@ -1,75 +1,76 @@ { - "name": "Uploads", - "description": "Upload & store blobs of data.", - "icon": "file-arrow-up", - "tags": [ - "core", "utility" - ], - "authors": [ - "rivet-gg", - "Blckbrry-Pi", - "NathanFlurry" - ], - "status": "stable", - "scripts": { - "prepare": { - "name": "Prepare Upload", - "description": "Prepare an upload batch for data transfer" - }, - "complete": { - "name": "Complete Upload", - "description": "Alert the module that the upload has been completed" - }, - "get": { - "name": "Get Upload Metadata", - "description": "Get the metadata (including contained files) for specified upload IDs" - }, - "get_public_file_urls": { - "name": "Get File Link", - "description": "Get presigned download links for each of the specified files" - }, - "delete": { - "name": "Delete Upload", - "description": "Removes the upload and deletes the files from the bucket" - } - }, - "errors": { - "no_files": { - "name": "No Files Provided", - "description": "An upload must have at least 1 file" - }, - "too_many_files": { - "name": "Too Many Files Provided", - "description": "There is a limit to how many files can be put into a single upload (see config)" - }, - "duplicate_paths": { - "name": "Duplicate Paths Provided", - "description": "An upload cannot contain 2 files with the same paths (see `cause` for offending paths)" - }, - "size_limit_exceeded": { - "name": "Combined Size Limit Exceeded", - "description": "There is a maximum total size per upload (see config)" - }, - "upload_not_found": { - "name": "Upload Not Found", - "description": "The provided upload ID didn't match any known existing uploads" - }, - "upload_already_completed": { - "name": "Upload Already completed", - "description": "\\`complete\\` was already called on this upload" - }, - "s3_not_configured": { - "name": "S3 Not Configured", - "description": "The S3 bucket is not configured (missing env variables)" - }, - "too_many_chunks": { - "name": "Possibility Of Too Many Chunks", - "description": "AWS S3 has a limit on the number of parts that can be uploaded in a\nmultipart upload. This limit is 10,000 parts. If the number of chunks\nrequired to upload the maximum multipart upload size exceeds this limit,\nany operation will preemptively throw this error.\n" - }, - "multipart_upload_completion_fail": { - "name": "Multipart Upload Completion Failure", - "description": "The multipart upload failed to complete (see `cause` for more information)" - } - }, - "dependencies": {} + "name": "Uploads", + "description": "Upload & store blobs of data.", + "icon": "file-arrow-up", + "tags": [ + "core", + "utility" + ], + "authors": [ + "rivet-gg", + "Blckbrry-Pi", + "NathanFlurry" + ], + "status": "stable", + "scripts": { + "prepare": { + "name": "Prepare Upload", + "description": "Prepare an upload batch for data transfer" + }, + "complete": { + "name": "Complete Upload", + "description": "Alert the module that the upload has been completed" + }, + "get": { + "name": "Get Upload Metadata", + "description": "Get the metadata (including contained files) for specified upload IDs" + }, + "get_public_file_urls": { + "name": "Get File Link", + "description": "Get presigned download links for each of the specified files" + }, + "delete": { + "name": "Delete Upload", + "description": "Removes the upload and deletes the files from the bucket" + } + }, + "errors": { + "no_files": { + "name": "No Files Provided", + "description": "An upload must have at least 1 file" + }, + "too_many_files": { + "name": "Too Many Files Provided", + "description": "There is a limit to how many files can be put into a single upload (see config)" + }, + "duplicate_paths": { + "name": "Duplicate Paths Provided", + "description": "An upload cannot contain 2 files with the same paths (see `cause` for offending paths)" + }, + "size_limit_exceeded": { + "name": "Combined Size Limit Exceeded", + "description": "There is a maximum total size per upload (see config)" + }, + "upload_not_found": { + "name": "Upload Not Found", + "description": "The provided upload ID didn't match any known existing uploads" + }, + "upload_already_completed": { + "name": "Upload Already completed", + "description": "\\`complete\\` was already called on this upload" + }, + "s3_not_configured": { + "name": "S3 Not Configured", + "description": "The S3 bucket is not configured (missing env variables)" + }, + "too_many_chunks": { + "name": "Possibility Of Too Many Chunks", + "description": "AWS S3 has a limit on the number of parts that can be uploaded in a\nmultipart upload. This limit is 10,000 parts. If the number of chunks\nrequired to upload the maximum multipart upload size exceeds this limit,\nany operation will preemptively throw this error.\n" + }, + "multipart_upload_completion_fail": { + "name": "Multipart Upload Completion Failure", + "description": "The multipart upload failed to complete (see `cause` for more information)" + } + }, + "dependencies": {} } diff --git a/modules/uploads/scripts/prepare.ts b/modules/uploads/scripts/prepare.ts index 69c06414..294287a8 100644 --- a/modules/uploads/scripts/prepare.ts +++ b/modules/uploads/scripts/prepare.ts @@ -1,5 +1,9 @@ -import { RuntimeError, ScriptContext, prisma } from "../module.gen.ts"; -import { PresignedUpload, prismaToOutput, MultipartUploadFile } from "../utils/types.ts"; +import { prisma, RuntimeError, ScriptContext } from "../module.gen.ts"; +import { + MultipartUploadFile, + PresignedUpload, + prismaToOutput, +} from "../utils/types.ts"; import { getPresignedMultipartUploadUrls, getPresignedPutUrl, @@ -97,12 +101,13 @@ export async function run( const uploadId = crypto.randomUUID(); const presignedInputFilePromises = req.files.map(async (file) => { if (file.multipart) { - const { chunks, multipartUploadId } = await getPresignedMultipartUploadUrls( - config.s3, - uploadId, - file, - getBytes(config.defaultMultipartChunkSize), - ); + const { chunks, multipartUploadId } = + await getPresignedMultipartUploadUrls( + config.s3, + uploadId, + file, + getBytes(config.defaultMultipartChunkSize), + ); return { ...file, diff --git a/modules/uploads/tests/e2e.ts b/modules/uploads/tests/e2e.ts index 2b10f08d..4fbe8ad3 100644 --- a/modules/uploads/tests/e2e.ts +++ b/modules/uploads/tests/e2e.ts @@ -7,10 +7,10 @@ import { faker } from "https://deno.land/x/deno_faker@v1.0.3/mod.ts"; import { getS3EnvConfig } from "../utils/env.ts"; test("e2e", async (ctx: TestContext) => { - if (!getS3EnvConfig()) { - ctx.log.warn("s3 not configured"); - return; - } + if (!getS3EnvConfig()) { + ctx.log.warn("s3 not configured"); + return; + } const path = faker.system.fileName(); const contentLength = String(faker.random.number(100)); diff --git a/modules/uploads/tests/multipart.ts b/modules/uploads/tests/multipart.ts index 3c4ec6ef..c3ee4cc9 100644 --- a/modules/uploads/tests/multipart.ts +++ b/modules/uploads/tests/multipart.ts @@ -19,10 +19,10 @@ function randomBuffer(size: number): Uint8Array { } test("multipart uploads", async (ctx: TestContext) => { - if (!getS3EnvConfig()) { - ctx.log.warn("s3 not configured"); - return; - } + if (!getS3EnvConfig()) { + ctx.log.warn("s3 not configured"); + return; + } const path = faker.system.fileName(); const contentLength = 20_000_000; // 20MB diff --git a/modules/uploads/utils/bucket.ts b/modules/uploads/utils/bucket.ts index 90c24b75..30a517fb 100644 --- a/modules/uploads/utils/bucket.ts +++ b/modules/uploads/utils/bucket.ts @@ -84,7 +84,9 @@ export async function getPresignedMultipartUploadUrls( file: UploadFile, chunkSize: bigint, expirySeconds = 60 * 60 * 6, -): Promise<{ key: string; chunks: PresignedChunk[]; multipartUploadId: string }> { +): Promise< + { key: string; chunks: PresignedChunk[]; multipartUploadId: string } +> { const client = getClient(config); const key = getKey(uploadId, file.path); diff --git a/modules/uploads/utils/config_defaults.ts b/modules/uploads/utils/config_defaults.ts index b7dfc4cd..2b0d3c0d 100644 --- a/modules/uploads/utils/config_defaults.ts +++ b/modules/uploads/utils/config_defaults.ts @@ -1,9 +1,9 @@ import { RuntimeError } from "../module.gen.ts"; import { Config as UserConfig } from "../config.ts"; -import { S3Config, getS3EnvConfig } from "./env.ts"; +import { getS3EnvConfig, S3Config } from "./env.ts"; import * as defaults from "../config.ts"; -import { UploadSize, confirmAwsChunkCount } from "./data_size.ts"; +import { confirmAwsChunkCount, UploadSize } from "./data_size.ts"; interface Config { maxUploadSize: UploadSize; diff --git a/modules/uploads/utils/env.ts b/modules/uploads/utils/env.ts index 2c0c861d..e3693e95 100644 --- a/modules/uploads/utils/env.ts +++ b/modules/uploads/utils/env.ts @@ -1,7 +1,7 @@ export interface S3EnvConfig { - S3_ENDPOINT: string; - S3_REGION: string; - S3_BUCKET: string; + S3_ENDPOINT: string; + S3_REGION: string; + S3_BUCKET: string; S3_ACCESS_KEY_ID: string; S3_SECRET_ACCESS_KEY: string; } @@ -14,7 +14,6 @@ export interface S3Config { secretAccessKey: string; } - export function getS3EnvConfig(): S3Config | null { const endpoint = Deno.env.get("S3_ENDPOINT"); const region = Deno.env.get("S3_REGION"); @@ -23,15 +22,15 @@ export function getS3EnvConfig(): S3Config | null { const secretAccessKey = Deno.env.get("S3_SECRET_ACCESS_KEY"); if ( - !endpoint || !region || !bucket || !accessKeyId || !secretAccessKey + !endpoint || !region || !bucket || !accessKeyId || !secretAccessKey ) { return null; } return { - endpoint, - region, - bucket, + endpoint, + region, + bucket, accessKeyId, secretAccessKey, }; diff --git a/modules/uploads/utils/types.ts b/modules/uploads/utils/types.ts index 50bae2c8..e1da3422 100644 --- a/modules/uploads/utils/types.ts +++ b/modules/uploads/utils/types.ts @@ -15,7 +15,7 @@ export interface Upload { /** * The total size of all files in the upload in bytes. - * + * * *(This is a string instead of a bigint because JSON doesn't support * serializing/deserializing bigints, and we want to be able to represent * very large file sizes.)* @@ -35,7 +35,7 @@ export interface UploadFile { /** * The size of the file in bytes. - * + * * *(This is a string instead of a bigint because JSON doesn't support * serializing/deserializing bigints, and we want to be able to represent * very large file sizes.)* @@ -75,7 +75,7 @@ export interface PresignedChunk { * This is ***not*** the total size of the file. * This is also ***not*** guaranteed to be the same as the `contentLength` * of all other chunks. - * + * * *(This is a string instead of a bigint because JSON doesn't support * serializing/deserializing bigints, and we want to be able to represent * very large file sizes.)* @@ -84,10 +84,10 @@ export interface PresignedChunk { /** * The offset of this chunk in the file. - * + * * Essentially, this chunk expects to represent the data from byte `offset` * to byte `offset + contentLength - 1` inclusive. - * + * * *(This is a string instead of a bigint because JSON doesn't support * serializing/deserializing bigints, and we want to be able to represent * very large file sizes.)* @@ -98,15 +98,19 @@ export interface PresignedChunk { type UploadWithoutFiles = Omit; type PrismaUploadWithoutFiles = Omit; -export type UploadWithOptionalFiles = UploadWithoutFiles & { files?: UploadFile[] }; -export type PrismaUploadWithOptionalFiles = PrismaUploadWithoutFiles & { files?: PrismaFiles[] }; +export type UploadWithOptionalFiles = UploadWithoutFiles & { + files?: UploadFile[]; +}; +export type PrismaUploadWithOptionalFiles = PrismaUploadWithoutFiles & { + files?: PrismaFiles[]; +}; export function prismaToOutput( upload: PrismaUploadWithOptionalFiles, ): UploadWithOptionalFiles { return { id: upload.id, - metadata: upload.metadata, + metadata: upload.metadata, bucket: upload.bucket, contentLength: upload.contentLength.toString(), diff --git a/modules/users/module.json b/modules/users/module.json index 4da4c724..1e3fdbc5 100644 --- a/modules/users/module.json +++ b/modules/users/module.json @@ -1,44 +1,44 @@ { - "name": "Users", - "description": "Identify and manage users.", - "icon": "user", - "tags": [ - "core", - "social" - ], - "authors": [ - "rivet-gg", - "NathanFlurry" - ], - "status": "stable", - "dependencies": { - "rate_limit": {}, - "tokens": {} - }, - "scripts": { - "fetch": { - "name": "Fetch User", - "public": true - }, - "create": { - "name": "Create User" - }, - "authenticate_token": { - "name": "Authenticate User Token", - "description": "Validate a user token. Throws an error if the token is invalid.", - "public": true - }, - "create_token": { - "name": "Create User Token", - "description": "Create a token for a user to authenticate future requests." - } - }, - "errors": { - "token_not_user_token": { - "name": "Token Not User Token" - }, - "unknown_identity_type": { - "name": "Unknown Identity Type" - } - } + "name": "Users", + "description": "Identify and manage users.", + "icon": "user", + "tags": [ + "core", + "social" + ], + "authors": [ + "rivet-gg", + "NathanFlurry" + ], + "status": "stable", + "dependencies": { + "rate_limit": {}, + "tokens": {} + }, + "scripts": { + "fetch": { + "name": "Fetch User", + "public": true + }, + "create": { + "name": "Create User" + }, + "authenticate_token": { + "name": "Authenticate User Token", + "description": "Validate a user token. Throws an error if the token is invalid.", + "public": true + }, + "create_token": { + "name": "Create User Token", + "description": "Create a token for a user to authenticate future requests." + } + }, + "errors": { + "token_not_user_token": { + "name": "Token Not User Token" + }, + "unknown_identity_type": { + "name": "Unknown Identity Type" + } + } } diff --git a/modules/users/scripts/authenticate_token.ts b/modules/users/scripts/authenticate_token.ts index e2e18269..5738fffe 100644 --- a/modules/users/scripts/authenticate_token.ts +++ b/modules/users/scripts/authenticate_token.ts @@ -4,12 +4,12 @@ import { User } from "../utils/types.ts"; export interface Request { userToken: string; - fetchUser?: boolean; + fetchUser?: boolean; } export interface Response { - userId: string; - user?: User; + userId: string; + user?: User; } export async function run( @@ -24,13 +24,12 @@ export async function run( if (token.type !== "user") throw new RuntimeError("token_not_user_token"); const userId = token.meta.userId; - let user; - if (req.fetchUser) { - user = await ctx.db.user.findFirstOrThrow({ - where: { id: userId }, - }); - - } + let user; + if (req.fetchUser) { + user = await ctx.db.user.findFirstOrThrow({ + where: { id: userId }, + }); + } return { userId, user }; }