Skip to content

feat: add azure blob storage upload#34

Open
lorenzocorallo wants to merge 4 commits into
mainfrom
azure-blob
Open

feat: add azure blob storage upload#34
lorenzocorallo wants to merge 4 commits into
mainfrom
azure-blob

Conversation

@lorenzocorallo
Copy link
Copy Markdown
Member

  • add azure blob storage client
  • add endpoint to change user profile picture

@lorenzocorallo lorenzocorallo requested a review from toto04 June 2, 2026 16:10
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Walkthrough

This PR adds Azure Blob Storage integration to enable user profile picture uploads. The version is bumped to 0.16.0 with the blob SDK dependency added. A new updateProfilePic TRPC mutation validates uploaded images and stores them in Azure, updating the user record with the image URL. The auth router is registered in the main TRPC app and the blob module is initialized at startup.

Changes

Profile Picture Upload via Azure Blob Storage

Layer / File(s) Summary
Version and dependency updates
package.json, package/package.json
Version bumped to 0.16.0; @azure/storage-blob SDK dependency added with constraint ^12.31.0.
Azure Blob Storage initialization and upload function
src/env.ts, src/azure/blob.ts
Environment variables AZURE_BLOB_STORAGE_ACCOUNT and AZURE_BLOB_STORAGE_CONTAINER added; blob service and container clients initialized via DefaultAzureCredential; uploadBlob function creates timestamped filenames, uploads buffers, and returns public URLs.
Profile picture mutation with validation and database update
src/routers/auth.ts
New authRouter with updateProfilePic mutation validates FormData (userId, image file), enforces PNG/JPEG types and 1MB limit, uploads via uploadBlob, updates user record with image URL, and returns success status.
Auth router registration and server initialization
src/routers/index.ts, src/server.ts
authRouter imported and registered under auth namespace in main TRPC app; blob module loaded at server startup.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add azure blob storage upload' clearly and concisely summarizes the main feature addition of Azure Blob Storage integration, which is the primary focus of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/azure/blob.ts`:
- Line 22: The upload call currently uses blobClient.upload(buffer,
buffer.length) which stores blobs as application/octet-stream; update the upload
invocation to pass the original MIME type via the upload options by supplying
blobHTTPHeaders.blobContentType (e.g., use blobClient.upload(buffer,
buffer.length, { blobHTTPHeaders: { blobContentType: contentType } })),
threading the original contentType/mimeType value from wherever the file/origin
is provided into the call so images render in browsers.
- Line 19: The filename generation using `const filename =
\`upload_${Date.now()}.${extension}\`` can collide under concurrent uploads;
modify the logic that builds `filename` to append or replace the timestamp with
a strong random component from the existing `nanoid` dependency (e.g., combine
`Date.now()` and `nanoid()` or use `nanoid()` alone) to guarantee uniqueness and
unpredictability, ensure `nanoid` is imported where `filename` is created
(import { nanoid } from 'nanoid'), and keep the file extension from `extension`
while producing a blob-safe string.

In `@src/routers/auth.ts`:
- Around line 9-29: The updateProfilePic handler currently defined as
publicProcedure trusts input.userId and updates SCHEMA.AUTH.user, enabling IDOR;
change it to use the authenticated identity (ctx.userId) instead of the input
userId or make it a protected/authenticated procedure and assert input.userId
=== ctx.userId before performing the DB.update in updateProfilePic; ensure you
reference the trpc context user id (ctx.userId from server.ts) and only call
DB.update(SCHEMA.AUTH.user).set(...) when the authenticated id matches the
target.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 43b933e9-bba2-4dbe-b64e-e0568d0b0730

📥 Commits

Reviewing files that changed from the base of the PR and between 07aa3c1 and 056726a.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • package.json
  • package/package.json
  • src/azure/blob.ts
  • src/env.ts
  • src/routers/auth.ts
  • src/routers/index.ts
  • src/server.ts

Comment thread src/azure/blob.ts
Comment on lines +10 to +24
export const storageClient = new BlobServiceClient(
`https://${storageAccount}.blob.core.windows.net`,
defaultAzureCredential
)

const containerName = env.AZURE_BLOB_STORAGE_CONTAINER
export const containerClient = storageClient.getContainerClient(containerName)

export async function uploadBlob(buffer: Buffer, extension: string) {
const filename = `upload_${Date.now()}.${extension}`
const blobClient = containerClient.getBlockBlobClient(filename)

const res = await blobClient.upload(buffer, buffer.length)
logger.info(`Upload block blob ${filename} successfully with request ID: ${res.requestId}`)
return { filename, url: `https://${storageAccount}.blob.core.windows.net/${containerName}/${filename}` }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Azure Blob Storage container public access disabled by default 2024 anonymous blob access

💡 Result:

Yes, Azure Blob Storage accounts and individual containers are configured to disable anonymous public access by default [1][2][3]. As of the most recent updates, Microsoft has enforced a security posture where anonymous access is restricted at both the storage account level and the container level out of the box [2][3]. Key details regarding this configuration include: 1. Storage Account Level: New Azure Storage accounts are created with the AllowBlobPublicAccess property set to False by default [3]. This setting acts as a master override; even if a specific container is configured to allow anonymous access, the storage account level setting prevents any anonymous requests from succeeding [2][4]. 2. Container Level: By default, individual containers are also set to private, meaning no anonymous access is permitted unless specifically authorized by a user with sufficient permissions [1][5]. 3. Enabling Access: To permit anonymous access, an administrator must first explicitly enable anonymous access for the entire storage account and subsequently configure the desired anonymous access level on individual containers [1][4]. 4. Security Best Practices: Because anonymous access presents a security risk, it is strongly recommended to use secure authorization methods such as Microsoft Entra ID (formerly Azure AD), Shared Access Signatures (SAS), or account keys instead of enabling public access [6][7][8]. Existing storage accounts created before these policy updates were rolled out may retain their previous configurations, but for all new deployments, the secure default is enforced [3]. Use Azure Policy to consistently enforce the restriction of anonymous access across your organization's storage environments [9].

Citations:


Fix blob URL accessibility (may 403 without SAS/auth)

uploadBlob returns a bare https://{account}.blob.core.windows.net/{container}/{filename} URL with no SAS token. Since Azure Storage anonymous/public blob access is disabled by default at the storage account/container level, unauthenticated clients will typically get 403/AuthenticationRequired unless anonymous read is explicitly enabled.

  • If containers are private, return a SAS URL (or otherwise provide an authenticated access path) instead of a “public” URL.
  • At minimum, ensure the container’s access level matches the assumption that this URL is publicly reachable.

Comment thread src/azure/blob.ts
export const containerClient = storageClient.getContainerClient(containerName)

export async function uploadBlob(buffer: Buffer, extension: string) {
const filename = `upload_${Date.now()}.${extension}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Timestamp-only filenames can collide and overwrite blobs.

upload_${Date.now()}.${extension} is not unique under concurrent uploads (same millisecond) and is predictable. nanoid is already a dependency — add a random component to avoid clobbering.

🔧 Proposed fix
-import { DefaultAzureCredential } from "`@azure/identity`"
-import { BlobServiceClient } from "`@azure/storage-blob`"
-import { env } from "`@/env`"
-import { logger } from "`@/logger`"
+import { DefaultAzureCredential } from "`@azure/identity`"
+import { BlobServiceClient } from "`@azure/storage-blob`"
+import { nanoid } from "nanoid"
+import { env } from "`@/env`"
+import { logger } from "`@/logger`"
-  const filename = `upload_${Date.now()}.${extension}`
+  const filename = `upload_${Date.now()}_${nanoid()}.${extension}`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/azure/blob.ts` at line 19, The filename generation using `const filename
= \`upload_${Date.now()}.${extension}\`` can collide under concurrent uploads;
modify the logic that builds `filename` to append or replace the timestamp with
a strong random component from the existing `nanoid` dependency (e.g., combine
`Date.now()` and `nanoid()` or use `nanoid()` alone) to guarantee uniqueness and
unpredictability, ensure `nanoid` is imported where `filename` is created
(import { nanoid } from 'nanoid'), and keep the file extension from `extension`
while producing a blob-safe string.

Comment thread src/azure/blob.ts
const filename = `upload_${Date.now()}.${extension}`
const blobClient = containerClient.getBlockBlobClient(filename)

const res = await blobClient.upload(buffer, buffer.length)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Set the blob content type on upload.

upload(buffer, buffer.length) stores the blob without blobContentType, so it defaults to application/octet-stream. Browsers will download rather than render the image, and the served URL won't behave as an image. Pass the original MIME type through.

🔧 Proposed fix (thread contentType through)
-export async function uploadBlob(buffer: Buffer, extension: string) {
+export async function uploadBlob(buffer: Buffer, extension: string, contentType: string) {
   const filename = `upload_${Date.now()}.${extension}`
   const blobClient = containerClient.getBlockBlobClient(filename)

-  const res = await blobClient.upload(buffer, buffer.length)
+  const res = await blobClient.upload(buffer, buffer.length, {
+    blobHTTPHeaders: { blobContentType: contentType },
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/azure/blob.ts` at line 22, The upload call currently uses
blobClient.upload(buffer, buffer.length) which stores blobs as
application/octet-stream; update the upload invocation to pass the original MIME
type via the upload options by supplying blobHTTPHeaders.blobContentType (e.g.,
use blobClient.upload(buffer, buffer.length, { blobHTTPHeaders: {
blobContentType: contentType } })), threading the original contentType/mimeType
value from wherever the file/origin is provided into the call so images render
in browsers.

Comment thread src/routers/auth.ts
Comment on lines +9 to +29
updateProfilePic: publicProcedure
.input(
z
.instanceof(FormData)
.transform((fd): Record<string, string | File> => Object.fromEntries(fd.entries()))
.pipe(
z.object({
userId: z.string(),
image: z
.file()
.mime(["image/png", "image/jpeg"])
.min(1)
.max(1024 * 1024), // 1MB
})
)
)
.mutation(async ({ input }) => {
try {
const buffer = Buffer.from(await input.image.arrayBuffer())
const file = await uploadBlob(buffer, input.image.type.includes("png") ? "png" : "jpeg")
await DB.update(SCHEMA.AUTH.user).set({ image: file.url }).where(eq(SCHEMA.AUTH.user.id, input.userId))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Broken access control: any unauthenticated caller can change any user's profile picture.

updateProfilePic is a publicProcedure that takes userId directly from input and updates SCHEMA.AUTH.user for that id. There is no authentication or authorization check, so anyone can overwrite an arbitrary user's image (IDOR). The TRPC context already exposes an identity (ctx.userId from the request header in server.ts), but it's ignored here.

Use a protected/authenticated procedure and derive the target from the authenticated session rather than trusting input.userId (or, at minimum, assert input.userId === ctx.userId).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routers/auth.ts` around lines 9 - 29, The updateProfilePic handler
currently defined as publicProcedure trusts input.userId and updates
SCHEMA.AUTH.user, enabling IDOR; change it to use the authenticated identity
(ctx.userId) instead of the input userId or make it a protected/authenticated
procedure and assert input.userId === ctx.userId before performing the DB.update
in updateProfilePic; ensure you reference the trpc context user id (ctx.userId
from server.ts) and only call DB.update(SCHEMA.AUTH.user).set(...) when the
authenticated id matches the target.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant