diff --git a/TODO.md b/TODO.md index d67fe56..59aef59 100644 --- a/TODO.md +++ b/TODO.md @@ -12,11 +12,6 @@ The format would be: ## Tasks -- [ ] Implement oRPC instead of Next.js Server Actions || Low - This would let us scale into using API calls for external usage of the app, to be used in other apps. - -- [ ] Use Tanstack Query for reloading data - - [ ] In the `CredentialMetadata` model changes The `additionalInfo` field should be a JSON object, of the user's choice. @@ -51,6 +46,9 @@ The format would be: - [ ] In each model that uses CRUD actions, implement `isDeleted` This would help in recovery and undo actions +- [ ] Use services instead of `lib` files + We should create a new folder called `services` and move all the functions that are not related to the database to this folder. + ### Finished Tasks - [x] Change return types of the Server Actions || High @@ -110,3 +108,8 @@ The format would be: - [x] usage of `Metadata` aserver actions - [x] Update dependecies: 1. Update `prisma` 2. Update `pnpm` + +- [x] Implement oRPC instead of Next.js Server Actions || Low + This would let us scale into using API calls for external usage of the app, to be used in other apps. + +- [x] Use Tanstack Query for reloading data diff --git a/actions/card/card-metadata.ts b/actions/card/card-metadata.ts deleted file mode 100644 index 5f7b59b..0000000 --- a/actions/card/card-metadata.ts +++ /dev/null @@ -1,347 +0,0 @@ -"use server" - -import { CardMetadataEntity } from "@/entities/card/card-metadata" -import { database } from "@/prisma/client" -import { - cardMetadataDtoSchema, - deleteCardMetadataDtoSchema, - getCardMetadataDtoSchema, - listCardMetadataDtoSchema, - updateCardMetadataDtoSchema, - type CardMetadataDto, - type CardMetadataSimpleRo, - type DeleteCardMetadataDto, - type GetCardMetadataDto, - type ListCardMetadataDto, - type UpdateCardMetadataDto, -} from "@/schemas/card/card-metadata" -import { z } from "zod" - -import { verifySession } from "@/lib/auth/verify" - -/** - * Create card metadata - */ -export async function createCardMetadata(data: CardMetadataDto): Promise<{ - success: boolean - metadata?: CardMetadataSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = cardMetadataDtoSchema.parse(data) - - // Check if card exists and belongs to the user - const card = await database.card.findFirst({ - where: { - id: validatedData.cardId, - userId: session.user.id, - }, - }) - - if (!card) { - return { - success: false, - error: "Card not found", - } - } - - // Check if metadata already exists for this card - const existingMetadata = await database.cardMetadata.findFirst({ - where: { cardId: validatedData.cardId }, - }) - - if (existingMetadata) { - return { - success: false, - error: "Metadata already exists for this card", - } - } - - // Create metadata - const metadata = await database.cardMetadata.create({ - data: validatedData, - }) - - return { - success: true, - metadata: CardMetadataEntity.getSimpleRo(metadata), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Card metadata creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get card metadata - */ -export async function getCardMetadata(data: GetCardMetadataDto): Promise<{ - success: boolean - metadata?: CardMetadataSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = getCardMetadataDtoSchema.parse(data) - - // Check if card exists and belongs to the user - const card = await database.card.findFirst({ - where: { - id: validatedData.cardId, - userId: session.user.id, - }, - }) - - if (!card) { - return { - success: false, - error: "Card not found", - } - } - - // Get metadata - const metadata = await database.cardMetadata.findFirst({ - where: { cardId: validatedData.cardId }, - }) - - if (!metadata) { - return { - success: false, - error: "Metadata not found for this card", - } - } - - return { - success: true, - metadata: CardMetadataEntity.getSimpleRo(metadata), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - console.error("Get card metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update card metadata - */ -export async function updateCardMetadata(data: UpdateCardMetadataDto): Promise<{ - success: boolean - metadata?: CardMetadataSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = updateCardMetadataDtoSchema.parse(data) - - // Get metadata - const existingMetadata = await database.cardMetadata.findUnique({ - where: { id: validatedData.id }, - include: { card: true }, - }) - - if (!existingMetadata) { - return { - success: false, - error: "Metadata not found", - } - } - - // Check if card belongs to the user - if (existingMetadata.card.userId !== session.user.id) { - return { - success: false, - error: "Not authorized", - } - } - - // Update metadata - const updatedMetadata = await database.cardMetadata.update({ - where: { id: validatedData.id }, - data: validatedData.data, - }) - - return { - success: true, - metadata: CardMetadataEntity.getSimpleRo(updatedMetadata), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Card metadata update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete card metadata - */ -export async function deleteCardMetadata(data: DeleteCardMetadataDto): Promise<{ - success: boolean - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = deleteCardMetadataDtoSchema.parse(data) - - // Get metadata - const existingMetadata = await database.cardMetadata.findUnique({ - where: { id: validatedData.id }, - include: { card: true }, - }) - - if (!existingMetadata) { - return { - success: false, - error: "Metadata not found", - } - } - - // Check if card belongs to the user - if (existingMetadata.card.userId !== session.user.id) { - return { - success: false, - error: "Not authorized", - } - } - - // Delete metadata - await database.cardMetadata.delete({ - where: { id: validatedData.id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - console.error("Delete card metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List card metadata by card ID - */ -export async function listCardMetadata(data: ListCardMetadataDto): Promise<{ - success: boolean - metadata?: CardMetadataSimpleRo[] - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = listCardMetadataDtoSchema.parse(data) - - // Check if card exists and belongs to the user - const card = await database.card.findFirst({ - where: { - id: validatedData.cardId, - userId: session.user.id, - }, - }) - - if (!card) { - return { - success: false, - error: "Card not found", - } - } - - // Get all metadata for this card - const metadata = await database.cardMetadata.findMany({ - where: { cardId: validatedData.cardId }, - }) - - return { - success: true, - metadata: metadata.map((item) => CardMetadataEntity.getSimpleRo(item)), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - console.error("List card metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/card/card.ts b/actions/card/card.ts deleted file mode 100644 index 328f516..0000000 --- a/actions/card/card.ts +++ /dev/null @@ -1,486 +0,0 @@ -"use server" - -import { CardEntity } from "@/entities/card/card" -import { database } from "@/prisma/client" -import { - CardDto, - cardDtoSchema, - CardSimpleRo, - DeleteCardDto, - deleteCardDtoSchema, - GetCardByIdDto, - getCardByIdDtoSchema, - UpdateCardDto, - updateCardDtoSchema, -} from "@/schemas/card" -import { CardMetadataDto } from "@/schemas/card/card-metadata" -import { Prisma } from "@prisma/client" -import { z } from "zod" - -import { verifySession } from "@/lib/auth/verify" -import { CardExpiryDateUtils } from "@/lib/card-expiry-utils" -import { getOrReturnEmptyObject } from "@/lib/utils" - -import { createEncryptedData } from "../encryption" -import { createTagsAndGetConnections } from "../utils/tag" -import { createCardMetadata } from "./card-metadata" - -/** - * Get card by ID (Simple RO) - */ -export async function getSimpleCardById(id: string): Promise<{ - success: boolean - card?: CardSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - const card = await database.card.findFirst({ - where: { - id, - userId: session.user.id, - }, - }) - - if (!card) { - return { - success: false, - error: "Card not found", - } - } - - return { - success: true, - card: CardEntity.getSimpleRo(card), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get simple card error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get card by ID (Full RO with relations) - */ -export async function getCardById(data: GetCardByIdDto): Promise<{ - success: boolean - card?: CardSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = getCardByIdDtoSchema.parse(data) - - // TODO: Placeholder for full RO with relations - // TODO: Update 'lastViewed' field - - const result = await getSimpleCardById(validatedData.id) - return result - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Get card error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a new card - */ -export async function createCard(data: CardDto): Promise<{ - success: boolean - card?: CardSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = cardDtoSchema.parse(data) - - // Handle expiry date using shared utility - const expiryDate = CardExpiryDateUtils.processServerExpiryDate( - validatedData.expiryDate - ) - - const tagConnections = await createTagsAndGetConnections( - validatedData.tags, - session.user.id, - validatedData.containerId - ) - - // Create encrypted data for CVV - const cvvEncryptionResult = await createEncryptedData({ - encryptedValue: validatedData.cvvEncryption.encryptedValue, - encryptionKey: validatedData.cvvEncryption.encryptionKey, - iv: validatedData.cvvEncryption.iv, - }) - - if (!cvvEncryptionResult.success || !cvvEncryptionResult.encryptedData) { - return { - success: false, - error: "Failed to encrypt CVV", - } - } - - // Create encrypted data for card number - const numberEncryptionResult = await createEncryptedData({ - encryptedValue: validatedData.numberEncryption.encryptedValue, - encryptionKey: validatedData.numberEncryption.encryptionKey, - iv: validatedData.numberEncryption.iv, - }) - - if ( - !numberEncryptionResult.success || - !numberEncryptionResult.encryptedData - ) { - return { - success: false, - error: "Failed to encrypt card number", - } - } - - const card = await database.card.create({ - data: { - name: validatedData.name, - description: validatedData.description, - type: validatedData.type, - provider: validatedData.provider, - status: validatedData.status, - expiryDate, - billingAddress: validatedData.billingAddress, - cardholderName: validatedData.cardholderName, - cardholderEmail: validatedData.cardholderEmail, - userId: session.user.id, - tags: tagConnections, - cvvEncryptionId: cvvEncryptionResult.encryptedData.id, - numberEncryptionId: numberEncryptionResult.encryptedData.id, - ...getOrReturnEmptyObject(validatedData.containerId, "containerId"), - }, - }) - - return { - success: true, - card: CardEntity.getSimpleRo(card), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Card creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update a card - */ -export async function updateCard(data: UpdateCardDto): Promise<{ - success: boolean - card?: CardSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = updateCardDtoSchema.parse(data) - const { id, ...updateData } = validatedData - - // Use getSimpleCardById to check if card exists and belongs to user - const existingCardResult = await getSimpleCardById(id) - if (!existingCardResult.success) { - return existingCardResult - } - - // Validate using our DTO schema (partial) - const partialCardSchema = cardDtoSchema.partial() - const validatedUpdateData = partialCardSchema.parse(updateData) - - const updatePayload: Record = {} - - // Handle expiry date if provided - if (validatedUpdateData.expiryDate) { - updatePayload.expiryDate = CardExpiryDateUtils.processServerExpiryDate( - validatedUpdateData.expiryDate - ) - } - - // Handle tags if provided - if (validatedUpdateData.tags) { - const tagConnections = await createTagsAndGetConnections( - validatedUpdateData.tags, - session.user.id, - validatedUpdateData.containerId - ) - updatePayload.tags = tagConnections - } - - // Handle CVV encryption update if provided - if (validatedUpdateData.cvvEncryption) { - const cvvEncryptionResult = await createEncryptedData({ - encryptedValue: validatedUpdateData.cvvEncryption.encryptedValue, - encryptionKey: validatedUpdateData.cvvEncryption.encryptionKey, - iv: validatedUpdateData.cvvEncryption.iv, - }) - - if (!cvvEncryptionResult.success || !cvvEncryptionResult.encryptedData) { - return { - success: false, - error: "Failed to encrypt CVV", - } - } - - updatePayload.cvvEncryptionId = cvvEncryptionResult.encryptedData.id - } - - // Handle number encryption update if provided - if (validatedUpdateData.numberEncryption) { - const numberEncryptionResult = await createEncryptedData({ - encryptedValue: validatedUpdateData.numberEncryption.encryptedValue, - encryptionKey: validatedUpdateData.numberEncryption.encryptionKey, - iv: validatedUpdateData.numberEncryption.iv, - }) - - if ( - !numberEncryptionResult.success || - !numberEncryptionResult.encryptedData - ) { - return { - success: false, - error: "Failed to encrypt card number", - } - } - - updatePayload.numberEncryptionId = numberEncryptionResult.encryptedData.id - } - - // Add other fields - Object.assign(updatePayload, { - name: validatedUpdateData.name, - description: validatedUpdateData.description, - type: validatedUpdateData.type, - provider: validatedUpdateData.provider, - status: validatedUpdateData.status, - billingAddress: validatedUpdateData.billingAddress, - cardholderName: validatedUpdateData.cardholderName, - cardholderEmail: validatedUpdateData.cardholderEmail, - ...getOrReturnEmptyObject(validatedUpdateData.containerId, "containerId"), - updatedAt: new Date(), - }) - - const updatedCard = await database.card.update({ - where: { id }, - data: updatePayload, - }) - - return { - success: true, - card: CardEntity.getSimpleRo(updatedCard), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Card update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete a card - */ -export async function deleteCard(data: DeleteCardDto): Promise<{ - success: boolean - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = deleteCardDtoSchema.parse(data) - - // Use getSimpleCardById to check if card exists and belongs to user - const existingCardResult = await getSimpleCardById(validatedData.id) - if (!existingCardResult.success) { - return { - success: false, - error: existingCardResult.error, - } - } - - await database.card.delete({ - where: { - id: validatedData.id, - userId: session.user.id, - }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Card deletion error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List cards with pagination - */ -export async function listCards( - page = 1, - limit = 10, - containerId?: string -): Promise<{ - success: boolean - cards?: CardSimpleRo[] - total?: number - error?: string -}> { - try { - const session = await verifySession() - const skip = (page - 1) * limit - - const whereClause: Prisma.CardWhereInput = { - userId: session.user.id, - ...(containerId && { containerId }), - } - - const [cards, total] = await Promise.all([ - database.card.findMany({ - where: whereClause, - skip, - take: limit, - orderBy: { createdAt: "desc" }, - }), - database.card.count({ where: whereClause }), - ]) - - return { - success: true, - cards: cards.map((card) => CardEntity.getSimpleRo(card)), - total, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("List cards error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a card with metadata - */ -export async function createCardWithMetadata( - cardData: CardDto, - metadataData?: Omit -): Promise<{ - success: boolean - card?: CardSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - // First create the card - const cardResult = await createCard(cardData) - - if (!cardResult.success || !cardResult.card) { - return cardResult - } - - // If metadata is provided, create it - if (metadataData) { - const metadataResult = await createCardMetadata({ - ...metadataData, - cardId: cardResult.card.id, - }) - - if (!metadataResult.success) { - // If metadata creation fails, we should probably delete the card - // But for now, we'll just return the card without metadata - console.warn( - "Card created but metadata creation failed:", - metadataResult.error - ) - } - } - - return cardResult - } catch (error) { - console.error("Create card with metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/card/index.ts b/actions/card/index.ts deleted file mode 100644 index ad441de..0000000 --- a/actions/card/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { - createCard, - getCardById, - updateCard, - deleteCard, - listCards, - createCardWithMetadata, -} from "./card" - -export { - createCardMetadata, - getCardMetadata, - updateCardMetadata, - deleteCardMetadata, - listCardMetadata, -} from "./card-metadata" diff --git a/actions/credential/credential-metadata.ts b/actions/credential/credential-metadata.ts deleted file mode 100644 index 5847741..0000000 --- a/actions/credential/credential-metadata.ts +++ /dev/null @@ -1,331 +0,0 @@ -"use server" - -import { CredentialMetadataEntity } from "@/entities/credential/credential-metadata" -import { database } from "@/prisma/client" -import { - credentialMetadataDtoSchema, - CredentialMetadataSimpleRo, - type CredentialMetadataDto as CredentialMetadataDtoType, -} from "@/schemas/credential" -import { z } from "zod" - -import { verifySession } from "@/lib/auth/verify" - -/** - * Create credential metadata - */ -export async function createCredentialMetadata( - data: CredentialMetadataDtoType -): Promise<{ - success: boolean - metadata?: CredentialMetadataSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = credentialMetadataDtoSchema.parse(data) - - try { - // Check if credential exists and belongs to the user - const credential = await database.credential.findFirst({ - where: { - id: validatedData.credentialId, - userId: session.user.id, - }, - }) - - if (!credential) { - return { - success: false, - error: "Credential not found", - } - } - - // Check if metadata already exists for this credential - const existingMetadata = await database.credentialMetadata.findFirst({ - where: { credentialId: validatedData.credentialId }, - }) - - if (existingMetadata) { - return { - success: false, - error: "Metadata already exists for this credential", - } - } - - // Create metadata with Prisma - const metadata = await database.credentialMetadata.create({ - data: { - ...validatedData, - }, - }) - - return { - success: true, - metadata: CredentialMetadataEntity.getSimpleRo(metadata), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Credential metadata creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get credential metadata - */ -export async function getCredentialMetadata(credentialId: string): Promise<{ - success: boolean - metadata?: CredentialMetadataSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - const credential = await database.credential.findFirst({ - where: { - id: credentialId, - userId: session.user.id, - }, - }) - - if (!credential) { - return { - success: false, - error: "Credential not found", - } - } - - // Get metadata - const metadata = await database.credentialMetadata.findFirst({ - where: { credentialId }, - }) - - if (!metadata) { - return { - success: false, - error: "Metadata not found for this credential", - } - } - - return { - success: true, - metadata: CredentialMetadataEntity.getSimpleRo(metadata), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get credential metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update credential metadata - */ -export async function updateCredentialMetadata( - id: string, - data: Partial -): Promise<{ - success: boolean - metadata?: CredentialMetadataSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - - // Get metadata - const existingMetadata = await database.credentialMetadata.findUnique({ - where: { id }, - include: { credential: true }, - }) - - if (!existingMetadata) { - return { - success: false, - error: "Metadata not found", - } - } - - // Check if credential belongs to the user - if (existingMetadata.credential.userId !== session.user.id) { - return { - success: false, - error: "Not authorized", - } - } - - // Validate using our DTO schema (partial) - const partialMetadataSchema = credentialMetadataDtoSchema.partial() - const validatedData = partialMetadataSchema.parse(data) - - try { - // Update metadata with Prisma - const updatedMetadata = await database.credentialMetadata.update({ - where: { id }, - data: validatedData, - }) - - return { - success: true, - metadata: CredentialMetadataEntity.getSimpleRo(updatedMetadata), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Credential metadata update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete credential metadata - */ -export async function deleteCredentialMetadata(id: string): Promise<{ - success: boolean - error?: string -}> { - try { - const session = await verifySession() - - // Get metadata - const existingMetadata = await database.credentialMetadata.findUnique({ - where: { id }, - include: { credential: true }, - }) - - if (!existingMetadata) { - return { - success: false, - error: "Metadata not found", - } - } - - // Check if credential belongs to the user - if (existingMetadata.credential.userId !== session.user.id) { - return { - success: false, - error: "Not authorized", - } - } - - // Delete metadata with Prisma - await database.credentialMetadata.delete({ - where: { id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Delete credential metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List credential metadata by credential ID - */ -export async function listCredentialMetadata(credentialId: string): Promise<{ - success: boolean - metadata?: CredentialMetadataSimpleRo[] - error?: string -}> { - try { - const session = await verifySession() - - // Check if credential exists and belongs to the user - const credential = await database.credential.findFirst({ - where: { - id: credentialId, - userId: session.user.id, - }, - }) - - if (!credential) { - return { - success: false, - error: "Credential not found", - } - } - - // Get all metadata for this credential - const metadata = await database.credentialMetadata.findMany({ - where: { credentialId }, - }) - - return { - success: true, - metadata: metadata.map((item) => - CredentialMetadataEntity.getSimpleRo(item) - ), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("List credential metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/credential/credential.ts b/actions/credential/credential.ts deleted file mode 100644 index 8b2daed..0000000 --- a/actions/credential/credential.ts +++ /dev/null @@ -1,518 +0,0 @@ -"use server" - -import { CredentialEntity } from "@/entities/credential/credential" -import { database } from "@/prisma/client" -import { - CredentialDto, - credentialDtoSchema, - CredentialSimpleRo, - DeleteCredentialDto, - deleteCredentialDtoSchema, - GetCredentialByIdDto, - getCredentialByIdDtoSchema, - UpdateCredentialDto, - updateCredentialDtoSchema, - type CredentialMetadataDto as CredentialMetadataDtoType, -} from "@/schemas/credential" -import { Prisma } from "@prisma/client" -import { z } from "zod" - -import { verifySession } from "@/lib/auth/verify" -import { getOrReturnEmptyObject } from "@/lib/utils" - -import { createEncryptedData } from "../encryption" -import { createTagsAndGetConnections } from "../utils/tag" - -/** - * Get credential by ID (Simple RO) - */ -export async function getSimpleCredentialById(id: string): Promise<{ - success: boolean - credential?: CredentialSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - const credential = await database.credential.findFirst({ - where: { - id, - userId: session.user.id, - }, - }) - - if (!credential) { - return { - success: false, - error: "Credential not found", - } - } - - return { - success: true, - credential: CredentialEntity.getSimpleRo(credential), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get simple credential error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get credential by ID (Full RO with relations) - */ -export async function getCredentialById(data: GetCredentialByIdDto): Promise<{ - success: boolean - credential?: CredentialSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = getCredentialByIdDtoSchema.parse(data) - - const result = await getSimpleCredentialById(validatedData.id) - - // Update last viewed timestamp if credential was found - if (result.success && result.credential) { - await database.credential.update({ - where: { id: validatedData.id }, - data: { lastViewed: new Date() }, - }) - } - - return result - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Get credential error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a new credential - */ -export async function createCredential(data: CredentialDto): Promise<{ - success: boolean - credential?: CredentialSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = credentialDtoSchema.parse(data) - - try { - const platform = await database.platform.findUnique({ - where: { id: validatedData.platformId }, - }) - - if (!platform) { - return { - success: false, - error: "Platform not found", - } - } - - const tagConnections = await createTagsAndGetConnections( - validatedData.tags, - session.user.id, - validatedData.containerId - ) - - // Create encrypted data for password - const passwordEncryptionResult = await createEncryptedData({ - encryptedValue: validatedData.passwordEncryption.encryptedValue, - encryptionKey: validatedData.passwordEncryption.encryptionKey, - iv: validatedData.passwordEncryption.iv, - }) - - if ( - !passwordEncryptionResult.success || - !passwordEncryptionResult.encryptedData - ) { - return { - success: false, - error: "Failed to encrypt password", - } - } - - const credential = await database.credential.create({ - data: { - identifier: validatedData.identifier, - passwordEncryptionId: passwordEncryptionResult.encryptedData.id, - status: validatedData.status, - platformId: validatedData.platformId, - description: validatedData.description, - userId: session.user.id, - tags: tagConnections, - ...getOrReturnEmptyObject(validatedData.containerId, "containerId"), - }, - include: { - passwordEncryption: true, - }, - }) - - return { - success: true, - credential: CredentialEntity.getSimpleRo(credential), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Credential creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update a credential - */ -export async function updateCredential(data: UpdateCredentialDto): Promise<{ - success: boolean - credential?: CredentialSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = updateCredentialDtoSchema.parse(data) - const { id, ...updateData } = validatedData - - // Use getSimpleCredentialById to check if credential exists and belongs to user - const existingCredentialResult = await getSimpleCredentialById(id) - if (!existingCredentialResult.success) { - return existingCredentialResult - } - - // Validate using our DTO schema (partial) - const partialCredentialSchema = credentialDtoSchema.partial() - const validatedUpdateData = partialCredentialSchema.parse(updateData) - - const updatePayload: Record = {} - - // Handle password encryption update if provided - if (validatedUpdateData.passwordEncryption) { - const passwordEncryptionResult = await createEncryptedData({ - encryptedValue: validatedUpdateData.passwordEncryption.encryptedValue, - encryptionKey: validatedUpdateData.passwordEncryption.encryptionKey, - iv: validatedUpdateData.passwordEncryption.iv, - }) - - if ( - !passwordEncryptionResult.success || - !passwordEncryptionResult.encryptedData - ) { - return { - success: false, - error: "Failed to encrypt password", - } - } - - updatePayload.passwordEncryptionId = - passwordEncryptionResult.encryptedData.id - - // Create credential history entry for password change - await database.credentialHistory.create({ - data: { - credentialId: id, - passwordEncryptionId: passwordEncryptionResult.encryptedData.id, - userId: session.user.id, - }, - }) - } - - // Handle tags if provided - if (validatedUpdateData.tags) { - const tagConnections = await createTagsAndGetConnections( - validatedUpdateData.tags, - session.user.id, - validatedUpdateData.containerId - ) - updatePayload.tags = tagConnections - } - - // Add other fields - Object.assign(updatePayload, { - identifier: validatedUpdateData.identifier, - status: validatedUpdateData.status, - description: validatedUpdateData.description, - platformId: validatedUpdateData.platformId, - ...getOrReturnEmptyObject(validatedUpdateData.containerId, "containerId"), - updatedAt: new Date(), - }) - - const updatedCredential = await database.credential.update({ - where: { id }, - data: updatePayload, - include: { - passwordEncryption: true, - }, - }) - - return { - success: true, - credential: CredentialEntity.getSimpleRo(updatedCredential), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Credential update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete a credential - */ -export async function deleteCredential(data: DeleteCredentialDto): Promise<{ - success: boolean - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = deleteCredentialDtoSchema.parse(data) - - // Use getSimpleCredentialById to check if credential exists and belongs to user - const existingCredentialResult = await getSimpleCredentialById( - validatedData.id - ) - if (!existingCredentialResult.success) { - return { - success: false, - error: existingCredentialResult.error, - } - } - - await database.credential.delete({ - where: { - id: validatedData.id, - userId: session.user.id, - }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Credential deletion error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List credentials with pagination - */ -export async function listCredentials( - page = 1, - limit = 10, - containerId?: string, - platformId?: string -): Promise<{ - success: boolean - credentials?: CredentialSimpleRo[] - total?: number - error?: string -}> { - try { - const session = await verifySession() - const skip = (page - 1) * limit - - const whereClause: Prisma.CredentialWhereInput = { - userId: session.user.id, - ...(containerId && { containerId }), - ...(platformId && { platformId }), - } - - const [credentials, total] = await Promise.all([ - database.credential.findMany({ - where: whereClause, - skip, - take: limit, - orderBy: { createdAt: "desc" }, - include: { - passwordEncryption: true, - }, - }), - database.credential.count({ where: whereClause }), - ]) - - return { - success: true, - credentials: credentials.map((credential) => - CredentialEntity.getSimpleRo(credential) - ), - total, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("List credentials error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Copy credential password (for clipboard functionality) - */ -export async function copyCredentialPassword(id: string): Promise<{ - success: boolean - error?: string -}> { - try { - const session = await verifySession() - - // Use getSimpleCredentialById to check if credential exists and belongs to user - const credentialResult = await getSimpleCredentialById(id) - if (!credentialResult.success) { - return credentialResult - } - - // Update last viewed timestamp - await database.credential.update({ - where: { id }, - data: { lastViewed: new Date() }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Copy credential password error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a credential with metadata - */ -export async function createCredentialWithMetadata( - credentialData: CredentialDto, - metadataData?: Omit -): Promise<{ - success: boolean - credential?: CredentialSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - // First create the credential - const credentialResult = await createCredential(credentialData) - - if (!credentialResult.success || !credentialResult.credential) { - return credentialResult - } - - // If metadata is provided, create it - if (metadataData) { - const { createCredentialMetadata } = await import("./credential-metadata") - - const metadataResult = await createCredentialMetadata({ - ...metadataData, - credentialId: credentialResult.credential.id, - }) - - if (!metadataResult.success) { - // If metadata creation fails, we should probably delete the credential - // But for now, we'll just return the credential without metadata - console.warn( - "Credential created but metadata creation failed:", - metadataResult.error - ) - } - } - - return credentialResult - } catch (error) { - console.error("Create credential with metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/credential/index.ts b/actions/credential/index.ts deleted file mode 100644 index 6144b5f..0000000 --- a/actions/credential/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { - createCredential, - getCredentialById, - updateCredential, - deleteCredential, - listCredentials, - copyCredentialPassword, - createCredentialWithMetadata, -} from "./credential" - -export { - createCredentialMetadata, - getCredentialMetadata, - updateCredentialMetadata, - deleteCredentialMetadata, - listCredentialMetadata, -} from "./credential-metadata" diff --git a/actions/encryption/card-encryption.ts b/actions/encryption/card-encryption.ts deleted file mode 100644 index 5db3a5f..0000000 --- a/actions/encryption/card-encryption.ts +++ /dev/null @@ -1,135 +0,0 @@ -"use server" - -import { EncryptedDataEntity } from "@/entities/encryption/encryption" -import { database } from "@/prisma/client" -import { EncryptedDataSimpleRo } from "@/schemas/encryption" - -import { verifySession } from "@/lib/auth/verify" - -/** - * Get CVV encrypted data for a card - */ -export async function getCardCvvEncryption(cardId: string): Promise<{ - success: boolean - encryptedData?: EncryptedDataSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - // First verify the card belongs to the user - const card = await database.card.findFirst({ - where: { - id: cardId, - userId: session.user.id, - }, - select: { - cvvEncryptionId: true, - }, - }) - - if (!card) { - return { - success: false, - error: "Card not found or not authorized", - } - } - - // Get the encrypted CVV data - const encryptedData = await database.encryptedData.findUnique({ - where: { - id: card.cvvEncryptionId, - }, - }) - - if (!encryptedData) { - return { - success: false, - error: "CVV encryption data not found", - } - } - - // TODO: Update 'lastViewed' field - - return { - success: true, - encryptedData: EncryptedDataEntity.getSimpleRo(encryptedData), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - - console.error("Get card CVV encryption error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get number encrypted data for a card - */ -export async function getCardNumberEncryption(cardId: string): Promise<{ - success: boolean - encryptedData?: EncryptedDataSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - // First verify the card belongs to the user - const card = await database.card.findFirst({ - where: { - id: cardId, - userId: session.user.id, - }, - select: { - numberEncryptionId: true, - }, - }) - - if (!card) { - return { - success: false, - error: "Card not found or not authorized", - } - } - - // Get the encrypted number data - const encryptedData = await database.encryptedData.findUnique({ - where: { - id: card.numberEncryptionId, - }, - }) - - if (!encryptedData) { - return { - success: false, - error: "Number encryption data not found", - } - } - - return { - success: true, - encryptedData: EncryptedDataEntity.getSimpleRo(encryptedData), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - - console.error("Get card number encryption error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/encryption/credential-encryption.ts b/actions/encryption/credential-encryption.ts deleted file mode 100644 index 66afb7d..0000000 --- a/actions/encryption/credential-encryption.ts +++ /dev/null @@ -1,74 +0,0 @@ -"use server" - -import { EncryptedDataEntity } from "@/entities/encryption/encryption" -import { database } from "@/prisma/client" -import { EncryptedDataSimpleRo } from "@/schemas/encryption" - -import { verifySession } from "@/lib/auth/verify" - -/** - * Get password encrypted data for a credential - */ -export async function getCredentialPasswordEncryption( - credentialId: string -): Promise<{ - success: boolean - encryptedData?: EncryptedDataSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - // First verify the credential belongs to the user - const credential = await database.credential.findFirst({ - where: { - id: credentialId, - userId: session.user.id, - }, - select: { - passwordEncryptionId: true, - }, - }) - - if (!credential) { - return { - success: false, - error: "Credential not found or not authorized", - } - } - - // Get the encrypted password data - const encryptedData = await database.encryptedData.findUnique({ - where: { - id: credential.passwordEncryptionId, - }, - }) - - if (!encryptedData) { - return { - success: false, - error: "Password encryption data not found", - } - } - - // TODO: Update the 'lastViewed' field - - return { - success: true, - encryptedData: EncryptedDataEntity.getSimpleRo(encryptedData), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - - console.error("Get credential password encryption error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/encryption/encrypted-data.ts b/actions/encryption/encrypted-data.ts deleted file mode 100644 index d3acc29..0000000 --- a/actions/encryption/encrypted-data.ts +++ /dev/null @@ -1,219 +0,0 @@ -"use server" - -import { database } from "@/prisma/client" -import { - EncryptedDataDto, - EncryptedDataSimpleRo, -} from "@/schemas/encryption/encryption" -import { z } from "zod" - -import { verifySession } from "@/lib/auth/verify" - -/** - * Create encrypted data - */ -export async function createEncryptedData(data: EncryptedDataDto): Promise<{ - success: boolean - encryptedData?: EncryptedDataSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - await verifySession() - - const encryptedData = await database.encryptedData.create({ - data: { - encryptedValue: data.encryptedValue, - encryptionKey: data.encryptionKey, - iv: data.iv, - }, - }) - - return { - success: true, - encryptedData: { - id: encryptedData.id, - encryptedValue: encryptedData.encryptedValue, - encryptionKey: encryptedData.encryptionKey, - iv: encryptedData.iv, - createdAt: encryptedData.createdAt, - updatedAt: encryptedData.updatedAt, - }, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Encrypted data creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update encrypted data - */ -export async function updateEncryptedData( - id: string, - data: EncryptedDataDto -): Promise<{ - success: boolean - encryptedData?: EncryptedDataSimpleRo - error?: string -}> { - try { - await verifySession() - - const encryptedData = await database.encryptedData.update({ - where: { id }, - data: { - encryptedValue: data.encryptedValue, - encryptionKey: data.encryptionKey, - iv: data.iv, - }, - }) - - return { - success: true, - encryptedData: { - id: encryptedData.id, - encryptedValue: encryptedData.encryptedValue, - encryptionKey: encryptedData.encryptionKey, - iv: encryptedData.iv, - createdAt: encryptedData.createdAt, - updatedAt: encryptedData.updatedAt, - }, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Encrypted data update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get encrypted data by ID - */ -export async function getEncryptedDataById(id: string): Promise<{ - success: boolean - encryptedData?: EncryptedDataSimpleRo - error?: string -}> { - try { - await verifySession() - - const encryptedData = await database.encryptedData.findUnique({ - where: { id }, - }) - - if (!encryptedData) { - return { - success: false, - error: "Encrypted data not found", - } - } - - // TODO: Update the 'lastViewed' field - - return { - success: true, - encryptedData: { - id: encryptedData.id, - encryptedValue: encryptedData.encryptedValue, - encryptionKey: encryptedData.encryptionKey, - iv: encryptedData.iv, - createdAt: encryptedData.createdAt, - updatedAt: encryptedData.updatedAt, - }, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get encrypted data error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete encrypted data - */ -export async function deleteEncryptedData(id: string): Promise<{ - success: boolean - error?: string -}> { - try { - await verifySession() - - const existingData = await database.encryptedData.findUnique({ - where: { id }, - }) - - if (!existingData) { - return { - success: false, - error: "Encrypted data not found", - } - } - - await database.encryptedData.delete({ - where: { id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Delete encrypted data error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -export async function listEncryptedDataCount(): Promise<{ - success: boolean - count?: number - error?: string -}> { - try { - const count = await database.encryptedData.count() - - return { - success: true, - count, - } - } catch (error) { - console.error("List encrypted data count error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/encryption/index.ts b/actions/encryption/index.ts deleted file mode 100644 index 95b1177..0000000 --- a/actions/encryption/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./encrypted-data" -export * from "./card-encryption" -export * from "./credential-encryption" -export * from "./secret-encryption" diff --git a/actions/encryption/secret-encryption.ts b/actions/encryption/secret-encryption.ts deleted file mode 100644 index fe48845..0000000 --- a/actions/encryption/secret-encryption.ts +++ /dev/null @@ -1,72 +0,0 @@ -"use server" - -import { EncryptedDataEntity } from "@/entities/encryption/encryption" -import { database } from "@/prisma/client" -import { EncryptedDataSimpleRo } from "@/schemas/encryption" - -import { verifySession } from "@/lib/auth/verify" - -/** - * Get value encrypted data for a secret - */ -export async function getSecretValueEncryption(secretId: string): Promise<{ - success: boolean - encryptedData?: EncryptedDataSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - // First verify the secret belongs to the user - const secret = await database.secret.findFirst({ - where: { - id: secretId, - userId: session.user.id, - }, - select: { - valueEncryptionId: true, - }, - }) - - if (!secret) { - return { - success: false, - error: "Secret not found or not authorized", - } - } - - // Get the encrypted value data - const encryptedData = await database.encryptedData.findUnique({ - where: { - id: secret.valueEncryptionId, - }, - }) - - if (!encryptedData) { - return { - success: false, - error: "Value encryption data not found", - } - } - - // TODO: Update the 'lastViewed' field - - return { - success: true, - encryptedData: EncryptedDataEntity.getSimpleRo(encryptedData), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - - console.error("Get secret value encryption error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/secrets/index.ts b/actions/secrets/index.ts deleted file mode 100644 index 8dd65bf..0000000 --- a/actions/secrets/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./secret" -export * from "./secret-metadata" diff --git a/actions/secrets/secret-metadata.ts b/actions/secrets/secret-metadata.ts deleted file mode 100644 index 46695e3..0000000 --- a/actions/secrets/secret-metadata.ts +++ /dev/null @@ -1,28 +0,0 @@ -"use server" - -// Placeholder for secret metadata operations -// TODO: Implement secret metadata CRUD operations - -export async function createSecretMetadata(/* _data: SecretMetadataDto */): Promise<{ - success: boolean - metadata?: object - error?: string -}> { - // TODO: Implement actual secret metadata creation - return { - success: false, - error: "Secret metadata creation not implemented yet", - } -} - -export async function getSecretMetadata() { - throw new Error("Not implemented yet") -} - -export async function updateSecretMetadata() { - throw new Error("Not implemented yet") -} - -export async function deleteSecretMetadata() { - throw new Error("Not implemented yet") -} diff --git a/actions/secrets/secret.ts b/actions/secrets/secret.ts deleted file mode 100644 index 6484d0e..0000000 --- a/actions/secrets/secret.ts +++ /dev/null @@ -1,860 +0,0 @@ -"use server" - -import { SecretEntity } from "@/entities/secrets/secret" -import { ContainerEntity } from "@/entities/utils/container" -import { database } from "@/prisma/client" -import { - DeleteSecretDto, - deleteSecretDtoSchema, - GetSecretByIdDto, - getSecretByIdDtoSchema, - SecretDto, - secretDtoSchema, - SecretSimpleRo, - UpdateSecretDto, - updateSecretDtoSchema, -} from "@/schemas/secrets/secret" -import { type SecretMetadataDto } from "@/schemas/secrets/secret-metadata" -import { EntityTypeEnum } from "@/schemas/utils" -import { - ContainerDto, - containerDtoSchema, - ContainerSimpleRo, -} from "@/schemas/utils/container" -import { Prisma, SecretStatus, SecretType } from "@prisma/client" -import { z } from "zod" - -import { verifySession } from "@/lib/auth/verify" - -import { createEncryptedData } from "../encryption" -import { containerSupportsEnvOperations } from "../utils/container" - -/** - * Get secret by ID (Simple RO) - */ -export async function getSimpleSecretById(id: string): Promise<{ - success: boolean - secret?: SecretSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - const secret = await database.secret.findFirst({ - where: { - id, - userId: session.user.id, - }, - include: { - valueEncryption: true, - }, - }) - - if (!secret) { - return { - success: false, - error: "Secret not found", - } - } - - // TODO: Update the 'lastViewed' field - - return { - success: true, - secret: SecretEntity.getSimpleRo(secret), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get simple secret error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get secret by ID (Full RO with relations) - */ -export async function getSecretById(data: GetSecretByIdDto): Promise<{ - success: boolean - secret?: SecretSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = getSecretByIdDtoSchema.parse(data) - - // TODO: Add the full RO with relations - const result = await getSimpleSecretById(validatedData.id) - - // Update last viewed timestamp if secret was found - if (result.success && result.secret) { - await database.secret.update({ - where: { id: validatedData.id }, - data: { lastViewed: new Date() }, - }) - } - - return result - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Get secret error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a new secret - */ -export async function createSecret(data: SecretDto): Promise<{ - success: boolean - secret?: SecretSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - - let validatedData: SecretDto - try { - validatedData = secretDtoSchema.parse(data) - } catch (validationError) { - if (validationError instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: validationError.issues, - } - } - throw validationError - } - - if (validatedData.containerId) { - const container = await database.container.findFirst({ - where: { - id: validatedData.containerId, - userId: session.user.id, - }, - }) - - if (!container) { - return { - success: false, - error: "Container not found", - } - } - - const isValid = ContainerEntity.validateEntityForContainer( - container.type, - EntityTypeEnum.SECRET - ) - - if (!isValid) { - return { - success: false, - error: `Cannot add secrets to ${container.type.toLowerCase().replace("_", " ")} container`, - } - } - } else { - console.log("No container ID provided") - return { - success: false, - error: "Container ID is required", - } - } - - let valueEncryptionResult - try { - valueEncryptionResult = await createEncryptedData({ - encryptedValue: validatedData.valueEncryption.encryptedValue, - encryptionKey: validatedData.valueEncryption.encryptionKey, - iv: validatedData.valueEncryption.iv, - }) - - if ( - !valueEncryptionResult.success || - !valueEncryptionResult.encryptedData - ) { - console.log("Encryption failed:", valueEncryptionResult) - return { - success: false, - error: "Failed to encrypt secret value", - } - } - - console.log("Encryption successful") - } catch (encryptionError) { - console.error("Encryption error:", encryptionError) - return { - success: false, - error: "Failed to encrypt secret value", - } - } - - console.log("Creating secret in database") - try { - const secret = await database.secret.create({ - data: { - name: validatedData.name, - valueEncryptionId: valueEncryptionResult.encryptedData.id, - userId: session.user.id, - containerId: validatedData.containerId, - note: validatedData.note, - }, - include: { - valueEncryption: true, - }, - }) - - console.log("Secret created successfully") - return { - success: true, - secret: SecretEntity.getSimpleRo(secret), - } - } catch (dbError) { - console.error("Database error:", dbError) - return { - success: false, - error: "Failed to create secret in database", - } - } - } catch (error) { - console.error("Secret creation error details:", error) - - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - console.log("Validation error:", error.issues) - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Secret creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update a secret - */ -export async function updateSecret(data: UpdateSecretDto): Promise<{ - success: boolean - secret?: SecretSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = updateSecretDtoSchema.parse(data) - const { id, ...updateData } = validatedData - - // Use getSimpleSecretById to check if secret exists and belongs to user - const existingSecretResult = await getSimpleSecretById(id) - if (!existingSecretResult.success) { - return existingSecretResult - } - - // Validate using our DTO schema (partial) - const partialSecretSchema = secretDtoSchema.partial() - const validatedUpdateData = partialSecretSchema.parse(updateData) - - const updatePayload: Record = {} - - // Handle value encryption update if provided - if (validatedUpdateData.valueEncryption) { - const valueEncryptionResult = await createEncryptedData({ - encryptedValue: validatedUpdateData.valueEncryption.encryptedValue, - encryptionKey: validatedUpdateData.valueEncryption.encryptionKey, - iv: validatedUpdateData.valueEncryption.iv, - }) - - if ( - !valueEncryptionResult.success || - !valueEncryptionResult.encryptedData - ) { - return { - success: false, - error: "Failed to encrypt secret value", - } - } - - updatePayload.valueEncryptionId = valueEncryptionResult.encryptedData.id - } - - // Validate container if being updated - if (validatedUpdateData.containerId) { - const container = await database.container.findFirst({ - where: { - id: validatedUpdateData.containerId, - userId: session.user.id, - }, - }) - - if (!container) { - return { - success: false, - error: "Container not found", - } - } - - if ( - !ContainerEntity.validateEntityForContainer( - container.type, - EntityTypeEnum.SECRET - ) - ) { - return { - success: false, - error: `Cannot add secrets to ${container.type.toLowerCase().replace("_", " ")} container`, - } - } - } - - // Add other fields - Object.assign(updatePayload, { - name: validatedUpdateData.name, - note: validatedUpdateData.note, - containerId: validatedUpdateData.containerId, - updatedAt: new Date(), - }) - - const updatedSecret = await database.secret.update({ - where: { id }, - data: updatePayload, - include: { - valueEncryption: true, - }, - }) - - return { - success: true, - secret: SecretEntity.getSimpleRo(updatedSecret), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Secret update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete a secret - */ -export async function deleteSecret(data: DeleteSecretDto): Promise<{ - success: boolean - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = deleteSecretDtoSchema.parse(data) - - // Use getSimpleSecretById to check if secret exists and belongs to user - const existingSecretResult = await getSimpleSecretById(validatedData.id) - if (!existingSecretResult.success) { - return { - success: false, - error: existingSecretResult.error, - } - } - - await database.secret.delete({ - where: { id: validatedData.id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Secret deletion error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List secrets with pagination - */ -export async function listSecrets( - page = 1, - limit = 10, - containerId?: string -): Promise<{ - success: boolean - secrets?: SecretSimpleRo[] - total?: number - error?: string -}> { - try { - const session = await verifySession() - const skip = (page - 1) * limit - - const whereClause: Prisma.SecretWhereInput = { - userId: session.user.id, - ...(containerId && { containerId }), - } - - const [secrets, total] = await Promise.all([ - database.secret.findMany({ - where: whereClause, - skip, - take: limit, - orderBy: { createdAt: "desc" }, - include: { - valueEncryption: true, - }, - }), - database.secret.count({ where: whereClause }), - ]) - - return { - success: true, - secrets: secrets.map((secret) => SecretEntity.getSimpleRo(secret)), - total, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("List secrets error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a secret with metadata - */ -export async function createSecretWithMetadata( - secretData: SecretDto, - metadataData?: Omit -): Promise<{ - success: boolean - secret?: SecretSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - // First create the secret - const secretResult = await createSecret(secretData) - - if (!secretResult.success || !secretResult.secret) { - return secretResult - } - - // If metadata is provided, create it - if (metadataData) { - const { createSecretMetadata } = await import("./secret-metadata") - - const metadataResult = await createSecretMetadata() - - if (!metadataResult.success) { - // If metadata creation fails, we should probably delete the secret - // But for now, we'll just return the secret without metadata - console.warn( - "Secret created but metadata creation failed:", - metadataResult.error - ) - } - } - - return secretResult - } catch (error) { - console.error("Create secret with metadata error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Generate .env file content from container secrets - */ -export async function generateEnvFile(containerId: string): Promise<{ - success: boolean - envContent?: string - error?: string -}> { - try { - const session = await verifySession() - - // Check if container supports environment operations - const supportsEnv = await containerSupportsEnvOperations(containerId) - if (!supportsEnv.success || !supportsEnv.supports) { - return { - success: false, - error: - supportsEnv.error || - "Container does not support environment operations", - } - } - - // Get all secrets in the container - const secrets = await database.secret.findMany({ - where: { - containerId, - userId: session.user.id, - }, - include: { - valueEncryption: true, - }, - orderBy: { name: "asc" }, - }) - - if (secrets.length === 0) { - return { - success: true, - envContent: "# No secrets found in this container\n", - } - } - - // Generate .env content - let envContent = `# Generated .env file from container\n# Generated on: ${new Date().toISOString()}\n\n` - - for (const secret of secrets) { - // Note: In a real implementation, you would decrypt the value here - // For now, we'll use a placeholder - envContent += `${secret.name.toUpperCase()}="[ENCRYPTED_VALUE]"\n` - } - - return { - success: true, - envContent, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Generate env file error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Generate .env.example file content from container secrets - */ -export async function generateEnvExampleFile(containerId: string): Promise<{ - success: boolean - envContent?: string - error?: string -}> { - try { - const session = await verifySession() - - // Check if container supports environment operations - const supportsEnv = await containerSupportsEnvOperations(containerId) - if (!supportsEnv.success || !supportsEnv.supports) { - return { - success: false, - error: - supportsEnv.error || - "Container does not support environment operations", - } - } - - // Get all secrets in the container - const secrets = await database.secret.findMany({ - where: { - containerId, - userId: session.user.id, - }, - orderBy: { name: "asc" }, - }) - - if (secrets.length === 0) { - return { - success: true, - envContent: "# No secrets found in this container\n", - } - } - - // Generate .env.example content - let envContent = `# Example environment file\n# Copy this file to .env and fill in your values\n# Generated on: ${new Date().toISOString()}\n\n` - - for (const secret of secrets) { - const note = secret.note ? ` # ${secret.note}` : "" - envContent += `${secret.name.toUpperCase()}=""${note}\n` - } - - return { - success: true, - envContent, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Generate env example file error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Generate T3 env.mjs file content from container secrets - */ -export async function generateT3EnvFile(containerId: string): Promise<{ - success: boolean - envContent?: string - error?: string -}> { - try { - const session = await verifySession() - - // Check if container supports environment operations - const supportsEnv = await containerSupportsEnvOperations(containerId) - if (!supportsEnv.success || !supportsEnv.supports) { - return { - success: false, - error: - supportsEnv.error || - "Container does not support environment operations", - } - } - - // Get all secrets in the container - const secrets = await database.secret.findMany({ - where: { - containerId, - userId: session.user.id, - }, - orderBy: { name: "asc" }, - }) - - if (secrets.length === 0) { - return { - success: true, - envContent: "// No secrets found in this container\n", - } - } - - // Generate T3 env.mjs content - let envContent = `// T3 Environment Configuration -// Generated on: ${new Date().toISOString()} - -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod"; - -export const env = createEnv({ - server: { -` - - for (const secret of secrets) { - const note = secret.note ? ` // ${secret.note}` : "" - envContent += ` ${secret.name.toUpperCase()}: z.string(),${note}\n` - } - - envContent += ` }, - client: { - // Add client-side environment variables here - }, - runtimeEnv: { -` - - for (const secret of secrets) { - envContent += ` ${secret.name.toUpperCase()}: process.env.${secret.name.toUpperCase()},\n` - } - - envContent += ` }, -}); -` - - return { - success: true, - envContent, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Generate T3 env file error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a container with multiple secrets in a single transaction - */ -export async function createContainerWithSecrets(data: { - container: ContainerDto - secrets: Array< - Omit & { - valueEncryption: { - encryptedValue: string - iv: string - encryptionKey: string - } - } - > -}): Promise<{ - success: boolean - container?: ContainerSimpleRo - secrets?: SecretSimpleRo[] - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - - const validatedContainer = containerDtoSchema.parse(data.container) - - return await database.$transaction(async (tx) => { - const container = await tx.container.create({ - data: { - name: validatedContainer.name, - icon: validatedContainer.icon, - description: validatedContainer.description, - type: validatedContainer.type, - userId: session.user.id, - ...(validatedContainer.tags.length > 0 && { - tags: { - create: validatedContainer.tags, - }, - }), - }, - }) - - const secrets = await Promise.all( - data.secrets.map(async (secret) => { - const valueEncryption = await tx.encryptedData.create({ - data: { - encryptedValue: secret.valueEncryption.encryptedValue, - iv: secret.valueEncryption.iv, - encryptionKey: secret.valueEncryption.encryptionKey, - }, - }) - - return tx.secret.create({ - data: { - name: secret.name, - note: secret.note, - valueEncryptionId: valueEncryption.id, - userId: session.user.id, - containerId: container.id, - metadata: { - create: [ - { - type: SecretType.API_KEY, - status: SecretStatus.ACTIVE, - otherInfo: [], - }, - ], - }, - }, - include: { - valueEncryption: true, - }, - }) - }) - ) - - return { - success: true, - container: ContainerEntity.getSimpleRo(container), - secrets: secrets.map((secret) => SecretEntity.getSimpleRo(secret)), - } - }) - } catch (error) { - console.error("Create container with secrets error:", error) - - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/user/index.ts b/actions/user/index.ts deleted file mode 100644 index 70ec546..0000000 --- a/actions/user/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./user" -export * from "./waitlist" diff --git a/actions/user/user.ts b/actions/user/user.ts deleted file mode 100644 index ac78e9f..0000000 --- a/actions/user/user.ts +++ /dev/null @@ -1,343 +0,0 @@ -"use server" - -import { database } from "@/prisma/client" -import { - deleteUserDtoSchema, - getUserByIdDtoSchema, - listUsersDtoSchema, - updateUserDtoSchema, - type DeleteUserDto, - type GetUserByIdDto, - type ListUsersDto, - type UpdateUserDto, -} from "@/schemas" -import { Prisma } from "@prisma/client" -import { z } from "zod" - -import { UserDto, UserRo, type UserDto as UserDtoType } from "@/config/schema" -import { verifySession } from "@/lib/auth/verify" - -/** - * Get user by ID (Simple RO) - * @todo: Use the UserEntity instead of the UserRo - */ -export async function getSimpleUserById(id: string): Promise<{ - success: boolean - user?: z.infer - error?: string -}> { - try { - const user = await database.user.findUnique({ - where: { id }, - }) - - if (!user) { - return { - success: false, - error: "User not found", - } - } - - return { - success: true, - user: UserRo.parse(user), - } - } catch (error) { - console.error("Get simple user error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get user by ID (Full RO with relations) - * @deprecated: We won't use this - * @todo: Use the UserEntity instead of the UserRo - */ -export async function getUserById(data: GetUserByIdDto): Promise<{ - success: boolean - user?: z.infer - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = getUserByIdDtoSchema.parse(data) - - const user = await database.user.findUnique({ - where: { id: validatedData.id }, - include: { - tags: true, - cards: true, - secrets: true, - platforms: true, - containers: true, - credentials: true, - credentialHistories: true, - }, - }) - - if (!user) { - return { - success: false, - error: "User not found", - } - } - - return { - success: true, - user: UserRo.parse(user), - } - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Get user error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get current authenticated user - */ -export async function getCurrentUser(): Promise<{ - success: boolean - user?: z.infer - error?: string -}> { - try { - const session = await verifySession() - - const result = await getSimpleUserById(session.user.id) - return result - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get current user error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a new user - */ -export async function createUser(data: UserDtoType): Promise<{ - success: boolean - user?: z.infer - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = UserDto.parse(data) - - try { - const user = await database.user.create({ - data: { - id: crypto.randomUUID(), - ...validatedData, - createdAt: new Date(), - updatedAt: new Date(), - }, - }) - - return { - success: true, - user: UserRo.parse(user), - } - } catch (error) { - if ( - error instanceof Prisma.PrismaClientKnownRequestError && - error.code === "P2002" - ) { - return { - success: false, - error: "A user with this email already exists", - } - } - throw error - } - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("User creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update a user - */ -export async function updateUser(data: UpdateUserDto): Promise<{ - success: boolean - user?: z.infer - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = updateUserDtoSchema.parse(data) - const { id, ...updateData } = validatedData - - // Use getSimpleUserById to check if user exists - const existingUserResult = await getSimpleUserById(id) - if (!existingUserResult.success) { - return existingUserResult - } - - try { - const updatedUser = await database.user.update({ - where: { id }, - data: { - ...updateData, - updatedAt: new Date(), - }, - }) - - return { - success: true, - user: UserRo.parse(updatedUser), - } - } catch (error) { - if ( - error instanceof Prisma.PrismaClientKnownRequestError && - error.code === "P2002" - ) { - return { - success: false, - error: "A user with this email already exists", - } - } - throw error - } - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("User update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete a user - */ -export async function deleteUser(data: DeleteUserDto): Promise<{ - success: boolean - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = deleteUserDtoSchema.parse(data) - - // Use getSimpleUserById to check if user exists - const existingUserResult = await getSimpleUserById(validatedData.id) - if (!existingUserResult.success) { - return { - success: false, - error: existingUserResult.error, - } - } - - await database.user.delete({ - where: { id: validatedData.id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("User deletion error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List users with pagination - */ -export async function listUsers( - data: ListUsersDto = { page: 1, limit: 10 } -): Promise<{ - success: boolean - users?: z.infer[] - total?: number - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = listUsersDtoSchema.parse(data) - const { page, limit } = validatedData - const skip = (page - 1) * limit - - const [users, total] = await Promise.all([ - database.user.findMany({ - skip, - take: limit, - orderBy: { createdAt: "desc" }, - }), - database.user.count(), - ]) - - return { - success: true, - users: users.map((user) => UserRo.parse(user)), - total, - } - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("List users error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/user/waitlist.ts b/actions/user/waitlist.ts deleted file mode 100644 index 846ec56..0000000 --- a/actions/user/waitlist.ts +++ /dev/null @@ -1,283 +0,0 @@ -"use server" - -import { database } from "@/prisma/client" -import { Prisma } from "@prisma/client" -import { z } from "zod" - -import { - WaitlistUserDtoSchema, - WaitlistUserRo, - type WaitlistUserDto, -} from "@/config/schema" - -const getWaitlistByIdDtoSchema = z.object({ - id: z.string().min(1, "Waitlist ID is required"), -}) - -const updateWaitlistDtoSchema = WaitlistUserDtoSchema.partial().extend({ - id: z.string().min(1, "Waitlist ID is required"), -}) - -const deleteWaitlistDtoSchema = z.object({ - id: z.string().min(1, "Waitlist ID is required"), -}) - -type GetWaitlistByIdDto = z.infer -type UpdateWaitlistDto = z.infer -type DeleteWaitlistDto = z.infer - -/** - * Get waitlist entry by ID (Simple RO) - */ -export async function getSimpleWaitlistById(id: string): Promise<{ - success: boolean - waitlist?: z.infer - error?: string -}> { - try { - const waitlist = await database.waitlist.findUnique({ - where: { id }, - }) - - if (!waitlist) { - return { - success: false, - error: "Waitlist entry not found", - } - } - - return { - success: true, - waitlist: WaitlistUserRo.parse(waitlist), - } - } catch (error) { - console.error("Get simple waitlist error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get waitlist entry by ID (Full RO) - */ -export async function getWaitlistById(data: GetWaitlistByIdDto): Promise<{ - success: boolean - waitlist?: z.infer - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = getWaitlistByIdDtoSchema.parse(data) - - const result = await getSimpleWaitlistById(validatedData.id) - return result - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Get waitlist error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Join waitlist server action - */ -export async function joinWaitlist(data: WaitlistUserDto): Promise<{ - success: boolean - waitlist?: z.infer - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = WaitlistUserDtoSchema.parse(data) - - try { - const waitlist = await database.waitlist.create({ - data: { - email: validatedData.email, - }, - }) - - return { - success: true, - waitlist: WaitlistUserRo.parse(waitlist), - } - } catch (error) { - if ( - error instanceof Prisma.PrismaClientKnownRequestError && - error.code === "P2002" - ) { - return { - success: false, - error: "This email is already on our waitlist", - } - } - throw error - } - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Waitlist submission error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update a waitlist entry - */ -export async function updateWaitlist(data: UpdateWaitlistDto): Promise<{ - success: boolean - waitlist?: z.infer - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = updateWaitlistDtoSchema.parse(data) - const { id, ...updateData } = validatedData - - // Use getSimpleWaitlistById to check if waitlist entry exists - const existingWaitlistResult = await getSimpleWaitlistById(id) - if (!existingWaitlistResult.success) { - return existingWaitlistResult - } - - try { - const updatedWaitlist = await database.waitlist.update({ - where: { id }, - data: updateData, - }) - - return { - success: true, - waitlist: WaitlistUserRo.parse(updatedWaitlist), - } - } catch (error) { - if ( - error instanceof Prisma.PrismaClientKnownRequestError && - error.code === "P2002" - ) { - return { - success: false, - error: "This email is already on our waitlist", - } - } - throw error - } - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Waitlist update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete a waitlist entry - */ -export async function deleteWaitlist(data: DeleteWaitlistDto): Promise<{ - success: boolean - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = deleteWaitlistDtoSchema.parse(data) - - // Use getSimpleWaitlistById to check if waitlist entry exists - const existingWaitlistResult = await getSimpleWaitlistById(validatedData.id) - if (!existingWaitlistResult.success) { - return { - success: false, - error: existingWaitlistResult.error, - } - } - - await database.waitlist.delete({ - where: { id: validatedData.id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Waitlist deletion error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List waitlist entries with pagination - */ -export async function listWaitlist( - page = 1, - limit = 10 -): Promise<{ - success: boolean - waitlist?: z.infer[] - total?: number - error?: string -}> { - try { - const skip = (page - 1) * limit - - const [waitlist, total] = await Promise.all([ - database.waitlist.findMany({ - skip, - take: limit, - orderBy: { createdAt: "desc" }, - }), - database.waitlist.count(), - ]) - - return { - success: true, - waitlist: waitlist.map((entry) => WaitlistUserRo.parse(entry)), - total, - } - } catch (error) { - console.error("List waitlist error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/utils/container.ts b/actions/utils/container.ts deleted file mode 100644 index 08de73a..0000000 --- a/actions/utils/container.ts +++ /dev/null @@ -1,482 +0,0 @@ -"use server" - -import { ContainerEntity, ContainerQuery } from "@/entities/utils/container" -import { database } from "@/prisma/client" -import { - ContainerDto, - containerDtoSchema, - ContainerSimpleRo, - DeleteContainerDto, - deleteContainerDtoSchema, - GetContainerByIdDto, - getContainerByIdDtoSchema, - UpdateContainerDto, - updateContainerDtoSchema, -} from "@/schemas/utils/container" -import { ContainerType } from "@prisma/client" -import { z } from "zod" - -import { verifySession } from "@/lib/auth/verify" - -/** - * Checks if container supports environment operations (only secrets-only or mixed with secrets) - */ -export async function containerSupportsEnvOperations( - containerId: string -): Promise<{ - success: boolean - supports?: boolean - error?: string -}> { - try { - const session = await verifySession() - - const container = await database.container.findFirst({ - where: { - id: containerId, - userId: session.user.id, - }, - include: ContainerQuery.getSecretsInclude(), - }) - - if (!container) { - return { - success: false, - error: "Container not found", - } - } - - if (container.type === ContainerType.SECRETS_ONLY) { - return { - success: true, - supports: true, - } - } - - if ( - container.type === ContainerType.MIXED && - container.secrets.length > 0 - ) { - return { - success: true, - supports: true, - } - } - - return { - success: true, - supports: false, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - return { - success: false, - error: "Failed to check container", - } - } -} - -/** - * Get container by ID (Simple RO) - */ -export async function getSimpleContainerById(id: string): Promise<{ - success: boolean - container?: ContainerSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - const container = await database.container.findFirst({ - where: { - id, - userId: session.user.id, - }, - }) - - if (!container) { - return { - success: false, - error: "Container not found", - } - } - - return { - success: true, - container: ContainerEntity.getSimpleRo(container), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get simple container error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get container by ID (Full RO with relations) - * @todo: Please implement the logic for the full-ro - */ -export async function getContainerById(data: GetContainerByIdDto): Promise<{ - success: boolean - container?: ContainerSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const validatedData = getContainerByIdDtoSchema.parse(data) - - const result = await getSimpleContainerById(validatedData.id) - return result - } catch (error) { - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Get container error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Create a new container - */ -export async function createContainer(data: ContainerDto): Promise<{ - success: boolean - container?: ContainerSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - - // Validate using our DTO schema - const validatedData = containerDtoSchema.parse(data) - - try { - // Extract tags from validatedData - const { tags, ...containerData } = validatedData - - // Create container with Prisma - const container = await database.container.create({ - data: { - ...containerData, - userId: session.user.id, - createdAt: new Date(), - updatedAt: new Date(), - ...(tags && - tags.length > 0 && { - tags: { - create: tags, - }, - }), - }, - }) - - return { - success: true, - container: ContainerEntity.getSimpleRo(container), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Container creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update a container - */ -export async function updateContainer(data: UpdateContainerDto): Promise<{ - success: boolean - container?: ContainerSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = updateContainerDtoSchema.parse(data) - const { id, ...updateData } = validatedData - - // Use getSimpleContainerById to check if container exists and belongs to user - const existingContainerResult = await getSimpleContainerById(id) - if (!existingContainerResult.success) { - return existingContainerResult - } - - // Validate using our DTO schema (partial) - const partialContainerSchema = containerDtoSchema.partial() - const validatedUpdateData = partialContainerSchema.parse(updateData) - - try { - // Extract tags from validatedUpdateData - const { tags, ...containerUpdateData } = validatedUpdateData - - // Update container with Prisma - const updatedContainer = await database.container.update({ - where: { id }, - data: { - ...containerUpdateData, - updatedAt: new Date(), - ...(tags && { - tags: { - deleteMany: {}, - create: tags, - }, - }), - }, - }) - - return { - success: true, - container: ContainerEntity.getSimpleRo(updatedContainer), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Container update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete a container - */ -export async function deleteContainer(data: DeleteContainerDto): Promise<{ - success: boolean - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - const validatedData = deleteContainerDtoSchema.parse(data) - - // Use getSimpleContainerById to check if container exists and belongs to user - const existingContainerResult = await getSimpleContainerById( - validatedData.id - ) - if (!existingContainerResult.success) { - return { - success: false, - error: existingContainerResult.error, - } - } - - // Check if container has any entities before deletion - const [credentialCount, secretCount, cardCount] = await Promise.all([ - database.credential.count({ - where: { containerId: validatedData.id }, - }), - database.secret.count({ - where: { containerId: validatedData.id }, - }), - database.card.count({ - where: { containerId: validatedData.id }, - }), - ]) - - if (credentialCount > 0 || secretCount > 0 || cardCount > 0) { - return { - success: false, - error: - "Cannot delete container with existing entities. Please move or delete all entities first.", - } - } - - await database.container.delete({ - where: { id: validatedData.id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Container deletion error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List containers with pagination - */ -export async function listContainers( - page = 1, - limit = 10 -): Promise<{ - success: boolean - containers?: ContainerSimpleRo[] - total?: number - error?: string -}> { - try { - const session = await verifySession() - const skip = (page - 1) * limit - - const [containers, total] = await Promise.all([ - database.container.findMany({ - where: { userId: session.user.id }, - skip, - take: limit, - orderBy: { createdAt: "desc" }, - }), - database.container.count({ - where: { userId: session.user.id }, - }), - ]) - - return { - success: true, - containers: containers.map((container) => - ContainerEntity.getSimpleRo(container) - ), - total, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("List containers error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get container statistics - */ -export async function getContainerStats(id: string): Promise<{ - success: boolean - stats?: { - credentialCount: number - secretCount: number - cardCount: number - tagCount: number - } - error?: string -}> { - try { - const session = await verifySession() - - // Use getSimpleContainerById to check if container exists and belongs to user - const containerResult = await getSimpleContainerById(id) - if (!containerResult.success) { - return containerResult - } - - const [credentialCount, secretCount, cardCount, tagCount] = - await Promise.all([ - database.credential.count({ - where: { containerId: id }, - }), - database.secret.count({ - where: { containerId: id }, - }), - database.card.count({ - where: { containerId: id }, - }), - database.tag.count({ - where: { containerId: id }, - }), - ]) - - return { - success: true, - stats: { - credentialCount, - secretCount, - cardCount, - tagCount, - }, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get container stats error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/utils/index.ts b/actions/utils/index.ts deleted file mode 100644 index 7ec766e..0000000 --- a/actions/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./container" -export * from "./tag" -export * from "./platform" diff --git a/actions/utils/platform.ts b/actions/utils/platform.ts deleted file mode 100644 index eed2101..0000000 --- a/actions/utils/platform.ts +++ /dev/null @@ -1,311 +0,0 @@ -"use server" - -import { headers } from "next/headers" -import { PlatformEntity } from "@/entities/utils/platform" -import { database } from "@/prisma/client" -import { - platformDtoSchema, - PlatformSimpleRo, - type PlatformDto as PlatformDtoType, -} from "@/schemas/utils/platform" -import { Prisma } from "@prisma/client" -import { z } from "zod" - -import { auth } from "@/lib/auth/server" -import { verifySession } from "@/lib/auth/verify" - -/** - * Create a new platform - */ -export async function createPlatform(data: PlatformDtoType): Promise<{ - success: boolean - platform?: PlatformSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - - // Validate using our DTO schema - const validatedData = platformDtoSchema.parse(data) - - try { - // Create platform with Prisma - const platform = await database.platform.create({ - data: { - ...validatedData, - userId: session.user.id, - createdAt: new Date(), - updatedAt: new Date(), - }, - }) - - return { - success: true, - platform: PlatformEntity.getSimpleRo(platform), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Platform creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get platform by ID - */ -export async function getPlatformById(id: string): Promise<{ - success: boolean - platform?: PlatformSimpleRo - error?: string -}> { - try { - const platform = await database.platform.findUnique({ - where: { id }, - }) - - if (!platform) { - return { - success: false, - error: "Platform not found", - } - } - - return { - success: true, - platform: PlatformEntity.getSimpleRo(platform), - } - } catch (error) { - console.error("Get platform error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update a platform - */ -export async function updatePlatform( - id: string, - data: Partial -): Promise<{ - success: boolean - platform?: PlatformSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - - // Make sure platform exists - const existingPlatform = await database.platform.findUnique({ - where: { id }, - }) - - if (!existingPlatform) { - return { - success: false, - error: "Platform not found", - } - } - - // Check ownership if platform has an owner - if ( - existingPlatform.userId && - existingPlatform.userId !== session.user.id - ) { - return { - success: false, - error: "Not authorized to update this platform", - } - } - - // Validate using our DTO schema (partial) - const partialPlatformSchema = platformDtoSchema.partial() - const validatedData = partialPlatformSchema.parse(data) - - try { - // Update platform with Prisma - const updatedPlatform = await database.platform.update({ - where: { id }, - data: { - ...validatedData, - updatedAt: new Date(), - }, - }) - - return { - success: true, - platform: PlatformEntity.getSimpleRo(updatedPlatform), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Platform update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete a platform - */ -export async function deletePlatform(id: string): Promise<{ - success: boolean - error?: string -}> { - try { - const session = await verifySession() - - // Make sure platform exists - const existingPlatform = await database.platform.findUnique({ - where: { id }, - }) - - if (!existingPlatform) { - return { - success: false, - error: "Platform not found", - } - } - - // Check ownership if platform has an owner - if ( - existingPlatform.userId && - existingPlatform.userId !== session.user.id - ) { - return { - success: false, - error: "Not authorized to delete this platform", - } - } - - // Check if platform is in use - const credentialCount = await database.credential.count({ - where: { platformId: id }, - }) - - if (credentialCount > 0) { - return { - success: false, - error: - "Cannot delete platform that is in use by credentials or secrets", - } - } - - // Delete platform with Prisma - await database.platform.delete({ - where: { id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Platform deletion error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List platforms with optional filtering and pagination - */ -export async function listPlatforms( - page = 1, - limit = 10, - includeSystem = true -): Promise<{ - success: boolean - platforms?: PlatformSimpleRo[] - total?: number - error?: string -}> { - try { - // Get authenticated user - const session = await auth.api.getSession({ - headers: await headers(), - }) - - const skip = (page - 1) * limit - - // Build filters based on whether to include system platforms - const where: Prisma.PlatformWhereInput = {} - - if (!includeSystem && session?.user?.id) { - where.userId = session.user.id - } - - const [platforms, total] = await Promise.all([ - database.platform.findMany({ - where, - skip, - take: limit, - orderBy: { - name: "asc", - }, - }), - database.platform.count({ where }), - ]) - - return { - success: true, - platforms: platforms.map((platform) => - PlatformEntity.getSimpleRo(platform) - ), - total, - } - } catch (error) { - console.error("List platforms error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/actions/utils/tag.ts b/actions/utils/tag.ts deleted file mode 100644 index cb82de1..0000000 --- a/actions/utils/tag.ts +++ /dev/null @@ -1,482 +0,0 @@ -"use server" - -import { TagEntity } from "@/entities/utils/tag" -import { database } from "@/prisma/client" -import { - tagDtoSchema, - TagSimpleRo, - type TagDto as TagDtoType, -} from "@/schemas/utils/tag" -import { Prisma } from "@prisma/client" -import { z } from "zod" - -import { verifySession } from "@/lib/auth/verify" -import { getOrReturnEmptyObject } from "@/lib/utils" - -/** - * Utility function to create tags and return connection objects - */ -export async function createTagsAndGetConnections( - tags: Array<{ name: string; color?: string }>, - userId: string, - containerId?: string -): Promise<{ connect: Array<{ id: string }> }> { - const tagPromises = tags.map(async (tag) => { - const result = await createTag({ - name: tag.name, - color: tag.color, - ...getOrReturnEmptyObject(containerId, "containerId"), - }) - return result.success ? result.tag : null - }) - - const createdTags = (await Promise.all(tagPromises)).filter( - (tag): tag is NonNullable => tag !== null - ) - - return { - connect: createdTags.map((tag) => ({ id: tag.id })), - } -} - -/** - * Create a new tag - */ -export async function createTag(data: TagDtoType): Promise<{ - success: boolean - tag?: TagSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - - // Validate using our DTO schema - const validatedData = tagDtoSchema.parse(data) - - try { - // Check if tag with same name already exists for this user - const existingTag = await database.tag.findFirst({ - where: { - name: validatedData.name, - userId: session.user.id, - }, - }) - - if (existingTag) { - return { - success: true, - tag: TagEntity.getSimpleRo(existingTag), - } - } - - // Create tag with Prisma - const tag = await database.tag.create({ - data: { - ...validatedData, - userId: session.user.id, - }, - }) - - return { - success: true, - tag: TagEntity.getSimpleRo(tag), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Tag creation error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Get tag by ID - */ -export async function getTagById(id: string): Promise<{ - success: boolean - tag?: TagSimpleRo - error?: string -}> { - try { - const session = await verifySession() - - const tag = await database.tag.findFirst({ - where: { - id, - userId: session.user.id, - }, - }) - - if (!tag) { - return { - success: false, - error: "Tag not found", - } - } - - return { - success: true, - tag: TagEntity.getSimpleRo(tag), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Get tag error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Update a tag - */ -export async function updateTag( - id: string, - data: Partial -): Promise<{ - success: boolean - tag?: TagSimpleRo - error?: string - issues?: z.ZodIssue[] -}> { - try { - const session = await verifySession() - - // Make sure tag exists and belongs to user - const existingTag = await database.tag.findFirst({ - where: { - id, - userId: session.user.id, - }, - }) - - if (!existingTag) { - return { - success: false, - error: "Tag not found", - } - } - - // Validate using our DTO schema (partial) - const partialTagSchema = tagDtoSchema.partial() - const validatedData = partialTagSchema.parse(data) - - try { - // Update tag with Prisma - const updatedTag = await database.tag.update({ - where: { id }, - data: validatedData, - }) - - return { - success: true, - tag: TagEntity.getSimpleRo(updatedTag), - } - } catch (error) { - throw error - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - if (error instanceof z.ZodError) { - return { - success: false, - error: "Validation failed", - issues: error.issues, - } - } - - console.error("Tag update error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Delete a tag - */ -export async function deleteTag(id: string): Promise<{ - success: boolean - error?: string -}> { - try { - const session = await verifySession() - - // Make sure tag exists and belongs to user - const existingTag = await database.tag.findFirst({ - where: { - id, - userId: session.user.id, - }, - }) - - if (!existingTag) { - return { - success: false, - error: "Tag not found", - } - } - - // Check if tag is in use by credentials - const credentialCount = await database.credential.count({ - where: { - tags: { - some: { - id, - }, - }, - }, - }) - - if (credentialCount > 0) { - return { - success: false, - error: "Cannot delete tag that is in use by credentials", - } - } - - // Delete tag with Prisma - await database.tag.delete({ - where: { id }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Tag deletion error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * List tags with optional filtering - */ -export async function listTags(containerId?: string): Promise<{ - success: boolean - tags?: TagSimpleRo[] - error?: string -}> { - try { - const session = await verifySession() - - // Build filters - const where: Prisma.TagWhereInput = { - userId: session.user.id, - } - - if (containerId) { - where.containerId = containerId - } - - const tags = await database.tag.findMany({ - where, - orderBy: { - name: "asc", - }, - }) - - return { - success: true, - tags: tags.map((tag) => TagEntity.getSimpleRo(tag)), - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("List tags error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Add tag to credential - */ -export async function addTagToCredential( - tagId: string, - credentialId: string -): Promise<{ - success: boolean - error?: string -}> { - try { - const session = await verifySession() - - // Make sure tag exists and belongs to user - const tag = await database.tag.findFirst({ - where: { - id: tagId, - userId: session.user.id, - }, - }) - - if (!tag) { - return { - success: false, - error: "Tag not found", - } - } - - // Make sure credential exists and belongs to user - const credential = await database.credential.findFirst({ - where: { - id: credentialId, - userId: session.user.id, - }, - }) - - if (!credential) { - return { - success: false, - error: "Credential not found", - } - } - - // Add tag to credential - await database.credential.update({ - where: { id: credentialId }, - data: { - tags: { - connect: { - id: tagId, - }, - }, - }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Add tag to credential error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} - -/** - * Remove tag from credential - */ -export async function removeTagFromCredential( - tagId: string, - credentialId: string -): Promise<{ - success: boolean - error?: string -}> { - try { - const session = await verifySession() - - // Make sure tag exists and belongs to user - const tag = await database.tag.findFirst({ - where: { - id: tagId, - userId: session.user.id, - }, - }) - - if (!tag) { - return { - success: false, - error: "Tag not found", - } - } - - // Make sure credential exists and belongs to user - const credential = await database.credential.findFirst({ - where: { - id: credentialId, - userId: session.user.id, - }, - }) - - if (!credential) { - return { - success: false, - error: "Credential not found", - } - } - - // Remove tag from credential - await database.credential.update({ - where: { id: credentialId }, - data: { - tags: { - disconnect: { - id: tagId, - }, - }, - }, - }) - - return { - success: true, - } - } catch (error) { - if (error instanceof Error && error.message === "Not authenticated") { - return { - success: false, - error: "Not authenticated", - } - } - console.error("Remove tag from credential error:", error) - return { - success: false, - error: "Something went wrong. Please try again.", - } - } -} diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index 2f249fd..0aeb939 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -1,4 +1,9 @@ import { Metadata } from "next" +import { createServerClient } from "@/orpc/client/server" +import { createContext } from "@/orpc/context" +import type { ListCardsOutput } from "@/schemas/card/dto" +import type { ListCredentialsOutput } from "@/schemas/credential/dto" +import type { ListSecretsOutput } from "@/schemas/secrets/dto" import { RecentItem, RecentItemTypeEnum } from "@/schemas/utils" import { MAX_RECENT_ITEMS } from "@/config/consts" @@ -7,18 +12,10 @@ import { mapItem } from "@/lib/utils" import { OverviewStats } from "@/components/app/dashboard-overview-stats" import { DashboardRecentActivity } from "@/components/app/dashboard-recent-activity" -import { listCards } from "@/actions/card" -import { listCredentials } from "@/actions/credential" -import { listSecrets } from "@/actions/secrets/secret" - -type CardsResponse = Awaited> -type SecretsResponse = Awaited> -type CredentialsResponse = Awaited> - async function getRecentItems( - usersResponse: CredentialsResponse, - cardsResponse: CardsResponse, - secretsResponse: SecretsResponse + usersResponse: ListCredentialsOutput, + cardsResponse: ListCardsOutput, + secretsResponse: ListSecretsOutput ): Promise { const recentCredentials: RecentItem[] = (usersResponse.credentials ?? []).map( (user) => ({ @@ -61,9 +58,9 @@ export const metadata: Metadata = { } async function getStats( - credentialsData: CredentialsResponse, - cardsData: CardsResponse, - secretsData: SecretsResponse + credentialsData: ListCredentialsOutput, + cardsData: ListCardsOutput, + secretsData: ListSecretsOutput ) { return { credentials: credentialsData.credentials?.length ?? 0, @@ -73,11 +70,23 @@ async function getStats( } export default async function DashboardPage() { + const context = await createContext() + const serverClient = createServerClient(context) + const [credentialsResponse, cardsResponse, secretsResponse] = await Promise.all([ - listCredentials(1, MAX_RECENT_ITEMS), - listCards(1, MAX_RECENT_ITEMS), - listSecrets(1, MAX_RECENT_ITEMS), + serverClient.credentials.list({ + page: 1, + limit: MAX_RECENT_ITEMS, + }), + serverClient.cards.list({ + page: 1, + limit: MAX_RECENT_ITEMS, + }), + serverClient.secrets.list({ + page: 1, + limit: MAX_RECENT_ITEMS, + }), ]) const stats = await getStats( diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx index 6985d75..a89fa11 100644 --- a/app/(marketing)/page.tsx +++ b/app/(marketing)/page.tsx @@ -1,3 +1,5 @@ +import { createServerClient } from "@/orpc/client/server" + import { MarketingFooter } from "@/components/app/marketing-footer" import { MarketingHeaderDesktop } from "@/components/app/marketing-header-desktop" import { MarketingHeaderMobile } from "@/components/app/marketing-header-mobile" @@ -5,16 +7,34 @@ import { MarketingWaitlistForm } from "@/components/app/marketing-waitlist-form" import { StatCard } from "@/components/shared/stat-card" import { AnimatedGridPattern } from "@/components/ui/animated-grid-pattern" -import { listEncryptedDataCount } from "@/actions/encryption" -import { listUsers } from "@/actions/user/user" -import { listWaitlist } from "@/actions/user/waitlist" - export default async function Home() { - const [waitlist, users, encryptedData] = await Promise.all([ - listWaitlist(), - listUsers(), - listEncryptedDataCount(), - ]) + const serverClient = createServerClient({ + session: null, + user: null, + }) + + let waitlist = { total: 0 } + let users = { total: 0 } + let encryptedData = { count: 0 } + + try { + const [waitlistResult, usersResult, encryptedDataResult] = + await Promise.all([ + serverClient.users.getWaitlistCount({}), + serverClient.users.getUserCount({}), + serverClient.users.getEncryptedDataCount({}), + ]) + + waitlist = waitlistResult + users = usersResult + encryptedData = encryptedDataResult + } catch (error) { + // Silently handle database connection errors during build + console.warn( + "Database not available during build, using default values:", + error + ) + } return (
diff --git a/app/api/orpc/[[...rest]]/route.ts b/app/api/orpc/[[...rest]]/route.ts new file mode 100644 index 0000000..de8e25d --- /dev/null +++ b/app/api/orpc/[[...rest]]/route.ts @@ -0,0 +1,37 @@ +import { createContext } from "@/orpc/context" +import { appRouter } from "@/orpc/routers" +import { RPCHandler } from "@orpc/server/fetch" + +const handler = new RPCHandler(appRouter) + +async function handleRequest(request: Request) { + try { + const { response } = await handler.handle(request, { + prefix: "/api/orpc", + context: await createContext(), + }) + + return response ?? new Response("Not found", { status: 404 }) + } catch (error) { + console.error("RPC handler error:", error) + + return new Response( + JSON.stringify({ + error: "Internal server error", + message: "An unexpected error occurred", + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ) + } +} + +export const GET = handleRequest +export const POST = handleRequest +export const PUT = handleRequest +export const PATCH = handleRequest +export const DELETE = handleRequest diff --git a/components/app/dashboard-add-card-dialog.tsx b/components/app/dashboard-add-card-dialog.tsx index 95d8733..4d2a877 100644 --- a/components/app/dashboard-add-card-dialog.tsx +++ b/components/app/dashboard-add-card-dialog.tsx @@ -1,6 +1,7 @@ "use client" import { useState } from "react" +import { useCreateCard } from "@/orpc/hooks" import { CardDto, cardDtoSchema } from "@/schemas/card" import { TagDto } from "@/schemas/utils/tag" import { zodResolver } from "@hookform/resolvers/zod" @@ -16,8 +17,6 @@ import { AddItemDialog } from "@/components/shared/add-item-dialog" import { Icons } from "@/components/shared/icons" import { Form } from "@/components/ui/form" -import { createCard } from "@/actions/card" - interface CardDialogProps { open: boolean onOpenChange: (open: boolean) => void @@ -30,9 +29,9 @@ export function DashboardAddCardDialog({ availableTags = [], }: CardDialogProps) { const { toast } = useToast() + const createCardMutation = useCreateCard() const [createMore, setCreateMore] = useState(false) - const [isSubmitting, setIsSubmitting] = useState(false) const [sensitiveData, setSensitiveData] = useState({ number: "", cvv: "", @@ -65,19 +64,17 @@ export function DashboardAddCardDialog({ }) async function onSubmit() { - try { - setIsSubmitting(true) - - if (!sensitiveData.number.trim()) { - toast("Card number is required", "error") - return - } + if (!sensitiveData.number.trim()) { + toast("Card number is required", "error") + return + } - if (!sensitiveData.cvv.trim()) { - toast("CVV is required", "error") - return - } + if (!sensitiveData.cvv.trim()) { + toast("CVV is required", "error") + return + } + try { const key = await generateEncryptionKey() const encryptCvvResult = await encryptData(sensitiveData.cvv, key) const encryptNumberResult = await encryptData(sensitiveData.number, key) @@ -116,60 +113,62 @@ export function DashboardAddCardDialog({ }, } - const result = await createCard(cardDataWithEncryption) - - if (result.success) { - toast("Card saved successfully", "success") - - if (!createMore) { - handleDialogOpenChange(false) - } else { - form.reset({ - name: "", - description: "", - type: CardType.CREDIT, - provider: CardProvider.VISA, - status: CardStatus.ACTIVE, - expiryDate: "", - billingAddress: "", - cardholderName: "", - cardholderEmail: "", - tags: [], - numberEncryption: { - encryptedValue: "", - iv: "", - encryptionKey: "", - }, - cvvEncryption: { - encryptedValue: "", - iv: "", - encryptionKey: "", - }, - }) - setSensitiveData({ number: "", cvv: "" }) - } - } else { - const errorDetails = result.issues - ? result.issues - .map((issue) => `${issue.path.join(".")}: ${issue.message}`) - .join(", ") - : result.error - - toast( - `Failed to save card: ${errorDetails || "Unknown error"}`, - "error" - ) - } + createCardMutation.mutate(cardDataWithEncryption, { + onSuccess: () => { + toast("Card saved successfully", "success") + + if (!createMore) { + handleDialogOpenChange(false) + } else { + form.reset({ + name: "", + description: "", + type: CardType.CREDIT, + provider: CardProvider.VISA, + status: CardStatus.ACTIVE, + expiryDate: "", + billingAddress: "", + cardholderName: "", + cardholderEmail: "", + tags: [], + numberEncryption: { + encryptedValue: "", + iv: "", + encryptionKey: "", + }, + cvvEncryption: { + encryptedValue: "", + iv: "", + encryptionKey: "", + }, + }) + setSensitiveData({ number: "", cvv: "" }) + } + }, + onError: (error) => { + const { message, details } = handleErrors( + error, + "Failed to save card" + ) + toast( + details + ? `${message}: ${Array.isArray(details) ? details.join(", ") : details}` + : message, + "error" + ) + }, + }) } catch (error) { - const { message, details } = handleErrors(error, "Failed to save card") + const { message, details } = handleErrors( + error, + "Failed to encrypt card data" + ) toast( details ? `${message}: ${Array.isArray(details) ? details.join(", ") : details}` : message, "error" ) - } finally { - setIsSubmitting(false) } } @@ -189,7 +188,7 @@ export function DashboardAddCardDialog({ title="Add New Card" description="Add a new card to your vault. All information is securely stored." icon={} - isSubmitting={isSubmitting} + isSubmitting={createCardMutation.isPending} createMore={createMore} onCreateMoreChange={setCreateMore} createMoreText="Create another card" diff --git a/components/app/dashboard-add-card-form.tsx b/components/app/dashboard-add-card-form.tsx index 02ec529..d6e6374 100644 --- a/components/app/dashboard-add-card-form.tsx +++ b/components/app/dashboard-add-card-form.tsx @@ -8,8 +8,8 @@ import { CardStatus } from "@prisma/client" import { ChevronDown, Plus } from "lucide-react" import { UseFormReturn } from "react-hook-form" -import { CardExpiryDateUtils } from "@/lib/card-expiry-utils" import { cn, getMetadataLabels } from "@/lib/utils" +import { CardExpiryDateUtils } from "@/lib/utils/card-expiry-helpers" import { CardPaymentInputs } from "@/components/shared/card-payment-inputs" import { CardStatusIndicator } from "@/components/shared/card-status-indicator" diff --git a/components/app/dashboard-add-credential-dialog.tsx b/components/app/dashboard-add-credential-dialog.tsx index d0bfc18..be1d51c 100644 --- a/components/app/dashboard-add-credential-dialog.tsx +++ b/components/app/dashboard-add-credential-dialog.tsx @@ -1,6 +1,11 @@ "use client" import { useEffect, useState } from "react" +import { + useCreateCredentialWithMetadata, + usePlatforms, + useTags, +} from "@/orpc/hooks" import { CredentialDto, credentialDtoSchema, @@ -12,11 +17,12 @@ import { AccountStatus } from "@prisma/client" import { useForm } from "react-hook-form" import { encryptData, exportKey, generateEncryptionKey } from "@/lib/encryption" -import { checkPasswordStrength, generatePassword } from "@/lib/password" import { cn, getMetadataLabels, handleErrors } from "@/lib/utils" +import { + checkPasswordStrength, + generatePassword, +} from "@/lib/utils/password-helpers" import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard" -import { usePlatforms } from "@/hooks/use-platforms" -import { useTags } from "@/hooks/use-tags" import { useToast } from "@/hooks/use-toast" import { DashboardAddCredentialForm } from "@/components/app/dashboard-add-credential-form" @@ -33,8 +39,6 @@ import { import { Form } from "@/components/ui/form" import { Separator } from "@/components/ui/separator" -import { createCredentialWithMetadata } from "@/actions/credential" - interface CredentialDialogProps { open: boolean onOpenChange: (open: boolean) => void @@ -45,12 +49,15 @@ export function DashboardAddCredentialDialog({ onOpenChange, }: CredentialDialogProps) { const { toast } = useToast() - const { platforms, error: platformsError } = usePlatforms() - const { tags: availableTags, error: tagsError } = useTags() + const platformsQuery = usePlatforms() + const tagsQuery = useTags() + const createCredentialWithMetadataMutation = useCreateCredentialWithMetadata() + + const platforms = platformsQuery.data?.platforms || [] + const availableTags = tagsQuery.data?.tags || [] const [createMore, setCreateMore] = useState(false) const [showMetadata, setShowMetadata] = useState(false) - const [isSubmitting, setIsSubmitting] = useState(false) const [passwordStrength, setPasswordStrength] = useState<{ score: number feedback: string @@ -95,13 +102,13 @@ export function DashboardAddCredentialDialog({ }) useEffect(() => { - if (platformsError) { - toast(platformsError, "error") + if (platformsQuery.error) { + toast("Failed to load platforms", "error") } - if (tagsError) { - toast(tagsError, "error") + if (tagsQuery.error) { + toast("Failed to load tags", "error") } - }, [platformsError, tagsError, toast]) + }, [platformsQuery.error, tagsQuery.error, toast]) const handleGeneratePassword = () => { const newPassword = generatePassword(16) @@ -140,19 +147,17 @@ export function DashboardAddCredentialDialog({ } async function onSubmit() { - try { - setIsSubmitting(true) - - if (!sensitiveData.identifier.trim()) { - toast("Identifier is required", "error") - return - } + if (!sensitiveData.identifier.trim()) { + toast("Identifier is required", "error") + return + } - if (!sensitiveData.password.trim()) { - toast("Password is required", "error") - return - } + if (!sensitiveData.password.trim()) { + toast("Password is required", "error") + return + } + try { const key = await generateEncryptionKey() const encryptResult = await encryptData(sensitiveData.password, key) const keyString = await exportKey(key as CryptoKey) @@ -214,57 +219,61 @@ export function DashboardAddCredentialDialog({ } } - const result = await createCredentialWithMetadata( - credentialDto, - metadataDto - ) - - if (result.success) { - toast("Credential saved successfully", "success") - - if (!createMore) { - handleDialogOpenChange(false) - } else { - credentialForm.reset({ - identifier: "", - description: "", - status: AccountStatus.ACTIVE, - platformId: credentialData.platformId, - containerId: credentialData.containerId, - passwordEncryption: { - encryptedValue: "", - iv: "", - encryptionKey: "", - }, - tags: [], - metadata: [], - }) - metadataForm.reset({ - recoveryEmail: "", - phoneNumber: "", - otherInfo: [], - has2FA: false, - }) - setSensitiveData({ identifier: "", password: "" }) - setPasswordStrength(null) - setShowMetadata(false) + createCredentialWithMetadataMutation.mutate( + { + credential: credentialDto, + metadata: metadataDto, + }, + { + onSuccess: () => { + toast("Credential saved successfully", "success") + + if (!createMore) { + handleDialogOpenChange(false) + } else { + credentialForm.reset({ + identifier: "", + description: "", + status: AccountStatus.ACTIVE, + platformId: credentialData.platformId, + containerId: credentialData.containerId, + passwordEncryption: { + encryptedValue: "", + iv: "", + encryptionKey: "", + }, + tags: [], + metadata: [], + }) + metadataForm.reset({ + recoveryEmail: "", + phoneNumber: "", + otherInfo: [], + has2FA: false, + }) + setSensitiveData({ identifier: "", password: "" }) + setPasswordStrength(null) + setShowMetadata(false) + } + }, + onError: (error) => { + const { message, details } = handleErrors( + error, + "Failed to save credential" + ) + toast( + details + ? `${message}: ${Array.isArray(details) ? details.join(", ") : details}` + : message, + "error" + ) + }, } - } else { - const errorDetails = result.issues - ? result.issues - .map((issue) => `${issue.path.join(".")}: ${issue.message}`) - .join(", ") - : result.error - - toast( - `Failed to save credential: ${errorDetails || "Unknown error"}`, - "error" - ) - } + ) } catch (error) { const { message, details } = handleErrors( error, - "Failed to save credential" + "Failed to encrypt credential data" ) toast( details @@ -272,8 +281,6 @@ export function DashboardAddCredentialDialog({ : message, "error" ) - } finally { - setIsSubmitting(false) } } @@ -296,7 +303,7 @@ export function DashboardAddCredentialDialog({ title="Add New Credential" description="Add a new credential to your vault. All information is securely stored." icon={} - isSubmitting={isSubmitting} + isSubmitting={createCredentialWithMetadataMutation.isPending} createMore={createMore} onCreateMoreChange={setCreateMore} createMoreText="Create another credential" @@ -316,7 +323,11 @@ export function DashboardAddCredentialDialog({ ({ + name: tag.name, + containerId: tag.containerId || undefined, + color: tag.color || undefined, + }))} passwordStrength={passwordStrength} sensitiveData={sensitiveData} setSensitiveData={setSensitiveData} diff --git a/components/app/dashboard-add-secret-dialog.tsx b/components/app/dashboard-add-secret-dialog.tsx index a0c884c..80dda9d 100644 --- a/components/app/dashboard-add-secret-dialog.tsx +++ b/components/app/dashboard-add-secret-dialog.tsx @@ -1,6 +1,7 @@ "use client" import { useState } from "react" +import { useCreateContainerWithSecrets } from "@/orpc/hooks" import { SecretDto, secretDtoSchema } from "@/schemas/secrets/secret" import { zodResolver } from "@hookform/resolvers/zod" import { ContainerType, SecretStatus, SecretType } from "@prisma/client" @@ -15,8 +16,6 @@ import { AddItemDialog } from "@/components/shared/add-item-dialog" import { Icons } from "@/components/shared/icons" import { Form } from "@/components/ui/form" -import { createContainerWithSecrets } from "@/actions/secrets/secret" - interface SecretDialogProps { open: boolean onOpenChange: (open: boolean) => void @@ -27,10 +26,10 @@ export function DashboardAddSecretDialog({ onOpenChange, }: SecretDialogProps) { const { toast } = useToast() + const createContainerWithSecretsMutation = useCreateContainerWithSecrets() const [title, setTitle] = useState("") const [createMore, setCreateMore] = useState(false) - const [isSubmitting, setIsSubmitting] = useState(false) const [sensitiveData, setSensitiveData] = useState({ value: "", }) @@ -58,21 +57,19 @@ export function DashboardAddSecretDialog({ }) async function onSubmit() { - try { - setIsSubmitting(true) - - if (!sensitiveData.value.trim()) { - toast("Secret value is required", "error") - return - } + if (!sensitiveData.value.trim()) { + toast("Secret value is required", "error") + return + } - const keyValuePairs = parseKeyValuePairs(sensitiveData.value) + const keyValuePairs = parseKeyValuePairs(sensitiveData.value) - if (keyValuePairs.length === 0) { - toast("No valid key-value pairs found", "error") - return - } + if (keyValuePairs.length === 0) { + toast("No valid key-value pairs found", "error") + return + } + try { // Encrypt all secrets on the client side const encryptedSecrets = await Promise.all( keyValuePairs.map(async (pair: { key: string; value: string }) => { @@ -92,64 +89,83 @@ export function DashboardAddSecretDialog({ }) ) - const result = await createContainerWithSecrets({ - container: { - name: title, - icon: "🔧", - description: form.getValues("note"), - type: ContainerType.SECRETS_ONLY, - tags: [], + createContainerWithSecretsMutation.mutate( + { + container: { + name: title, + icon: "🔧", + description: form.getValues("note"), + type: ContainerType.SECRETS_ONLY, + tags: [], + }, + secrets: encryptedSecrets, }, - secrets: encryptedSecrets, - }) - - if (result.success) { - toast( - `Successfully created ${result.secrets?.length || 0} secrets`, - "success" - ) - - if (!createMore) { - handleDialogOpenChange(false) - } else { - form.reset({ - name: "", - note: "", - valueEncryption: { - encryptedValue: "", - iv: "", - encryptionKey: "", - }, - metadata: [ - { - type: SecretType.API_KEY, - status: SecretStatus.ACTIVE, - otherInfo: [], - secretId: "", - }, - ], - containerId: "", - }) - setSensitiveData({ value: "" }) - setTitle("") + { + onSuccess: (result) => { + if (result.success) { + toast( + `Successfully created ${result.secrets?.length || 0} secrets`, + "success" + ) + + if (!createMore) { + handleDialogOpenChange(false) + } else { + form.reset({ + name: "", + note: "", + valueEncryption: { + encryptedValue: "", + iv: "", + encryptionKey: "", + }, + metadata: [ + { + type: SecretType.API_KEY, + status: SecretStatus.ACTIVE, + otherInfo: [], + secretId: "", + }, + ], + containerId: "", + }) + setSensitiveData({ value: "" }) + setTitle("") + } + } else { + toast( + `Failed to create secrets: ${result.error || "Unknown error"}`, + "error" + ) + } + }, + onError: (error) => { + console.error("Error in onSubmit:", error) + const { message, details } = handleErrors( + error, + "Failed to save secret" + ) + toast( + details + ? `${message}: ${Array.isArray(details) ? details.join(", ") : details}` + : message, + "error" + ) + }, } - } else { - toast( - `Failed to create secrets: ${result.error || "Unknown error"}`, - "error" - ) - } + ) } catch (error) { - console.error("Error in onSubmit:", error) - const { message, details } = handleErrors(error, "Failed to save secret") + console.error("Error in encryption:", error) + const { message, details } = handleErrors( + error, + "Failed to encrypt secret" + ) toast( details ? `${message}: ${Array.isArray(details) ? details.join(", ") : details}` : message, "error" ) - } finally { - setIsSubmitting(false) } } @@ -170,7 +186,7 @@ export function DashboardAddSecretDialog({ title="Add New Secret" description="Add a new secret to your vault. All information is securely stored." icon={} - isSubmitting={isSubmitting} + isSubmitting={createContainerWithSecretsMutation.isPending} createMore={createMore} onCreateMoreChange={setCreateMore} createMoreText="Create another secret" diff --git a/components/app/dashboard-overview-stats.tsx b/components/app/dashboard-overview-stats.tsx index 20ff568..5c3c48a 100644 --- a/components/app/dashboard-overview-stats.tsx +++ b/components/app/dashboard-overview-stats.tsx @@ -3,24 +3,6 @@ import * as React from "react" import { StatsCard } from "@/components/app/dashboard-overview-stats-card" import { Icons } from "@/components/shared/icons" -// import { listCards } from "@/actions/card" -// import { listSecrets } from "@/actions/secret" -// import { listUsers } from "@/actions/user" - -// async function getStats() { -// const [usersData, cardsData, secretsData] = await Promise.all([ -// listUsers(1, 1), // We only need the total, so limit to 1 -// listCards(1, 1), -// listSecrets(1, 1), -// ]) - -// return { -// accounts: usersData.total ?? 0, -// cards: cardsData.total ?? 0, -// secrets: secretsData.total ?? 0, -// } -// } - interface OverviewStatsProps { stats: { credentials: number @@ -30,8 +12,6 @@ interface OverviewStatsProps { } export async function OverviewStats({ stats }: OverviewStatsProps) { - // const stats = await getStats() - return (
({ resolver: zodResolver(WaitlistUserDtoSchema), @@ -31,22 +29,19 @@ export function MarketingWaitlistForm({ count }: { count: number }) { }) async function onSubmit(values: WaitlistUserDto) { - setIsLoading(true) - - try { - const result = await joinWaitlist(values) - - if (result.success) { - toast.success("You've been added to our waitlist.") - form.reset() - } else { - toast.error(result.error || "Something went wrong") - } - } catch { - toast.error("Something went wrong. Please try again.") - } finally { - setIsLoading(false) - } + joinWaitlistMutation.mutate(values, { + onSuccess: (result) => { + if (result.success) { + toast.success("You've been added to our waitlist.") + form.reset() + } else { + toast.error(result.error || "Something went wrong") + } + }, + onError: () => { + toast.error("Something went wrong. Please try again.") + }, + }) } return ( @@ -73,7 +68,7 @@ export function MarketingWaitlistForm({ count }: { count: number }) { @@ -84,10 +79,12 @@ export function MarketingWaitlistForm({ count }: { count: number }) { diff --git a/components/layout/layout-wrapper.tsx b/components/layout/layout-wrapper.tsx index e8dc748..78ec76d 100644 --- a/components/layout/layout-wrapper.tsx +++ b/components/layout/layout-wrapper.tsx @@ -1,12 +1,23 @@ "use client" +import { useState } from "react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ThemeProvider } from "@/components/layout/theme-provider" import { TooltipProvider } from "@/components/ui/tooltip" export function LayoutWrapper({ children }: { children: React.ReactNode }) { - const queryClient = new QueryClient() + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1 minute + refetchOnWindowFocus: false, + }, + }, + }) + ) return ( diff --git a/hooks/use-platforms.ts b/hooks/use-platforms.ts deleted file mode 100644 index 74dbc8a..0000000 --- a/hooks/use-platforms.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from "react" -import { PlatformSimpleRo } from "@/schemas/utils/platform" - -import { listPlatforms } from "@/actions/utils" - -export function usePlatforms() { - const [platforms, setPlatforms] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - const fetchPlatforms = async () => { - try { - const result = await listPlatforms(1, 100) - if (result.success && result.platforms) { - setPlatforms(result.platforms) - } else { - setError(result.error || "Failed to fetch platforms") - } - } catch (err) { - setError("An error occurred while fetching platforms") - } finally { - setIsLoading(false) - } - } - - fetchPlatforms() - }, []) - - return { platforms, isLoading, error } -} diff --git a/hooks/use-tags.ts b/hooks/use-tags.ts deleted file mode 100644 index ca644cb..0000000 --- a/hooks/use-tags.ts +++ /dev/null @@ -1,47 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" -import { TagDto } from "@/schemas/utils/tag" - -import { listTags } from "@/actions/utils" - -export function useTags(containerId?: string) { - const [tags, setTags] = useState([]) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(true) - - useEffect(() => { - const fetchTags = async () => { - try { - setIsLoading(true) - setError(null) - const result = await listTags(containerId) - - if (result.success && result.tags) { - const tagDtos = result.tags.map((tag) => ({ - name: tag.name, - color: tag.color || undefined, - userId: tag.userId || undefined, - containerId: tag.containerId || undefined, - })) - setTags(tagDtos) - } else { - setError(result.error || "Failed to fetch tags") - } - } catch (err) { - setError("An unexpected error occurred") - console.error("Error fetching tags:", err) - } finally { - setIsLoading(false) - } - } - - fetchTags() - }, [containerId]) - - return { - tags, - error, - isLoading, - } -} diff --git a/lib/card-expiry-utils.ts b/lib/utils/card-expiry-helpers.ts similarity index 100% rename from lib/card-expiry-utils.ts rename to lib/utils/card-expiry-helpers.ts diff --git a/lib/utils/encryption-helpers.ts b/lib/utils/encryption-helpers.ts new file mode 100644 index 0000000..8697b0b --- /dev/null +++ b/lib/utils/encryption-helpers.ts @@ -0,0 +1,29 @@ +import { database } from "@/prisma/client" +import type { EncryptedDataDto } from "@/schemas/encryption/encryption" + +export async function createEncryptedData(data: EncryptedDataDto): Promise<{ + success: boolean + encryptedData?: { id: string } + error?: string +}> { + try { + const encryptedData = await database.encryptedData.create({ + data: { + iv: data.iv, + encryptedValue: data.encryptedValue, + encryptionKey: data.encryptionKey, + }, + }) + + return { + success: true, + encryptedData: { id: encryptedData.id }, + } + } catch (error) { + console.error("Error creating encrypted data:", error) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred", + } + } +} diff --git a/lib/utils.ts b/lib/utils/index.ts similarity index 99% rename from lib/utils.ts rename to lib/utils/index.ts index 3bd9a95..c6d0dc3 100644 --- a/lib/utils.ts +++ b/lib/utils/index.ts @@ -21,6 +21,9 @@ import { KeyValuePair, User as UserType } from "@/types" import { PRIORITY_ACTIVITY_TYPE } from "@/config/consts" +export * from "./card-expiry-helpers" +export * from "./password-helpers" + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } diff --git a/lib/password.ts b/lib/utils/password-helpers.ts similarity index 100% rename from lib/password.ts rename to lib/utils/password-helpers.ts diff --git a/lib/utils/tag-helpers.ts b/lib/utils/tag-helpers.ts new file mode 100644 index 0000000..7b4f8f6 --- /dev/null +++ b/lib/utils/tag-helpers.ts @@ -0,0 +1,49 @@ +import { database } from "@/prisma/client" +import type { TagDto } from "@/schemas/utils/tag" +import type { Prisma, Tag } from "@prisma/client" + +export async function createTagsAndGetConnections( + tags: TagDto[], + userId: string, + containerId?: string +): Promise { + if (!tags || tags.length === 0) { + return { connect: [] } + } + + const existingTags = await database.tag.findMany({ + where: { + name: { in: tags.map((tag) => tag.name) }, + userId, + containerId: containerId || null, + }, + }) + + const existingTagNames = new Set(existingTags.map((tag) => tag.name)) + const tagsToCreate = tags.filter((tag) => !existingTagNames.has(tag.name)) + + let newTags: Tag[] = [] + if (tagsToCreate.length > 0) { + const createData = tagsToCreate.map((tag) => ({ + name: tag.name, + color: tag.color, + userId, + containerId: containerId || null, + })) + + await database.tag.createMany({ data: createData }) + + newTags = await database.tag.findMany({ + where: { + name: { in: tagsToCreate.map((tag) => tag.name) }, + userId, + containerId: containerId || null, + }, + }) + } + + const allTags = [...existingTags, ...newTags] + const tagConnections = allTags.map((tag) => ({ id: tag.id })) + + return { connect: tagConnections } +} diff --git a/middleware/auth.ts b/middleware/auth.ts new file mode 100644 index 0000000..1567a71 --- /dev/null +++ b/middleware/auth.ts @@ -0,0 +1,33 @@ +import type { AuthenticatedContext, PublicContext } from "@/orpc/types" +import { ORPCError } from "@orpc/server" +import type { MiddlewareNextFn } from "@orpc/server" + +export const publicMiddleware = async ({ + context, + next, +}: { + context: PublicContext + next: MiddlewareNextFn +}) => { + return next({ context }) +} + +export const authMiddleware = async ({ + context, + next, +}: { + context: PublicContext + next: MiddlewareNextFn +}) => { + if (!context.session || !context.user) { + throw new ORPCError("UNAUTHORIZED") + } + + return next({ + context: { + ...context, + session: context.session, + user: context.user, + }, + }) +} diff --git a/middleware/index.ts b/middleware/index.ts new file mode 100644 index 0000000..c9d8191 --- /dev/null +++ b/middleware/index.ts @@ -0,0 +1 @@ +export { publicMiddleware, authMiddleware } from "./auth" diff --git a/orpc/client/index.ts b/orpc/client/index.ts new file mode 100644 index 0000000..73260a2 --- /dev/null +++ b/orpc/client/index.ts @@ -0,0 +1,3 @@ +export { orpc } from "./utils" +export { rpcClient } from "./rpc" +export { createQueryClient, getQueryClient } from "./query" diff --git a/orpc/client/query.ts b/orpc/client/query.ts new file mode 100644 index 0000000..e27394f --- /dev/null +++ b/orpc/client/query.ts @@ -0,0 +1,57 @@ +import { QueryClient } from "@tanstack/react-query" + +export const createQueryClient = () => { + return new QueryClient({ + defaultOptions: { + queries: { + // Stale time - how long until data is considered stale + staleTime: 60 * 1000, // 1 minute + // Cache time - how long to keep data in cache after it's unused + gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime) + // Retry failed requests + retry: (failureCount, error: any) => { + // Don't retry on 4xx errors + // Check various error object structures defensively + const errorCode = error?.data?.code || error?.code + const errorStatus = error?.status || error?.data?.status + + if ( + errorCode === "UNAUTHORIZED" || + errorCode === "FORBIDDEN" || + errorStatus === 401 || + errorStatus === 403 + ) { + return false + } + // Retry up to 3 times for other errors + return failureCount < 3 + }, + // Refetch on window focus for important data + refetchOnWindowFocus: false, + // Refetch on reconnect + refetchOnReconnect: true, + }, + mutations: { + // Retry failed mutations once + retry: 1, + }, + }, + }) +} + +// Singleton instance for client-side +let queryClient: QueryClient | undefined + +export const getQueryClient = () => { + if (typeof window === "undefined") { + // Server: always create a new query client + return createQueryClient() + } + + // Client: create a new query client if we don't have one + if (!queryClient) { + queryClient = createQueryClient() + } + + return queryClient +} diff --git a/orpc/client/rpc.ts b/orpc/client/rpc.ts new file mode 100644 index 0000000..34c5260 --- /dev/null +++ b/orpc/client/rpc.ts @@ -0,0 +1,16 @@ +import { createORPCClient } from "@orpc/client" +import { RPCLink } from "@orpc/client/fetch" +import type { RouterClient } from "@orpc/server" + +import type { AppRouter } from "../routers" + +// Create the RPC link +const link = new RPCLink({ + url: `${process.env.NEXT_PUBLIC_APP_URL}/api/orpc`, + headers: { + "Content-Type": "application/json", + }, +}) + +// Create the oRPC client with proper typing +export const rpcClient: RouterClient = createORPCClient(link) diff --git a/orpc/client/server.ts b/orpc/client/server.ts new file mode 100644 index 0000000..bc07fe8 --- /dev/null +++ b/orpc/client/server.ts @@ -0,0 +1,19 @@ +import { createRouterClient } from "@orpc/server" +import type { RouterClient } from "@orpc/server" + +import { appRouter } from "../routers" +import type { ORPCContext } from "../types" + +/** + * Server-side oRPC client for SSR optimization + * This eliminates HTTP requests during server-side rendering + */ +export function createServerClient( + context: ORPCContext +): RouterClient { + return createRouterClient(appRouter, { + context: () => context, + }) +} + +export type ServerClient = RouterClient diff --git a/orpc/client/utils.ts b/orpc/client/utils.ts new file mode 100644 index 0000000..a78b7e3 --- /dev/null +++ b/orpc/client/utils.ts @@ -0,0 +1,7 @@ +import { createTanstackQueryUtils } from "@orpc/tanstack-query" + +import { rpcClient } from "./rpc" + +export const orpc = createTanstackQueryUtils(rpcClient, { + path: ["orpc"], +}) diff --git a/orpc/context.ts b/orpc/context.ts new file mode 100644 index 0000000..f303f17 --- /dev/null +++ b/orpc/context.ts @@ -0,0 +1,24 @@ +import { headers } from "next/headers" + +import { auth } from "@/lib/auth/server" + +import type { ORPCContext } from "./types" + +export async function createContext(): Promise { + try { + const authResult = await auth.api.getSession({ + headers: await headers(), + }) + + return { + session: authResult?.session || null, + user: authResult?.user || null, + } + } catch (error) { + console.error("Failed to get session:", error) + return { + session: null, + user: null, + } + } +} diff --git a/orpc/hooks/index.ts b/orpc/hooks/index.ts new file mode 100644 index 0000000..5907bfb --- /dev/null +++ b/orpc/hooks/index.ts @@ -0,0 +1,7 @@ +export * from "./use-cards" +export * from "./use-containers" +export * from "./use-credentials" +export * from "./use-platforms" +export * from "./use-secrets" +export * from "./use-tags" +export * from "./use-users" diff --git a/orpc/hooks/use-cards.ts b/orpc/hooks/use-cards.ts new file mode 100644 index 0000000..872fceb --- /dev/null +++ b/orpc/hooks/use-cards.ts @@ -0,0 +1,149 @@ +"use client" + +import { orpc } from "@/orpc/client" +import type { + CardOutput, + CreateCardInput, + DeleteCardInput, + ListCardsInput, + ListCardsOutput, + UpdateCardInput, +} from "@/schemas/card/dto" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" + +// Query keys factory +export const cardKeys = { + all: ["cards"] as const, + lists: () => [...cardKeys.all, "list"] as const, + list: (filters: Partial) => + [...cardKeys.lists(), filters] as const, + details: () => [...cardKeys.all, "detail"] as const, + detail: (id: string) => [...cardKeys.details(), id] as const, +} + +// Get single card +export function useCard(id: string) { + return useQuery({ + queryKey: cardKeys.detail(id), + queryFn: () => orpc.cards.get.call({ id }), + enabled: !!id, + }) +} + +// List cards with pagination +export function useCards(input: ListCardsInput = { page: 1, limit: 10 }) { + return useQuery({ + queryKey: cardKeys.list(input), + queryFn: () => orpc.cards.list.call(input), + placeholderData: (previousData) => previousData, + }) +} + +// Create card mutation +export function useCreateCard() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: CreateCardInput) => orpc.cards.create.call(input), + onSuccess: (newCard: CardOutput) => { + // Invalidate and refetch card lists + queryClient.invalidateQueries({ queryKey: cardKeys.lists() }) + + // Add the new card to the cache + queryClient.setQueryData(cardKeys.detail(newCard.id), newCard) + }, + onError: (error) => { + console.error("Failed to create card:", error) + }, + }) +} + +// Update card mutation +export function useUpdateCard() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: UpdateCardInput) => orpc.cards.update.call(input), + onMutate: async (input) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: cardKeys.detail(input.id), + }) + + // Snapshot the previous value + const previousCard = queryClient.getQueryData( + cardKeys.detail(input.id) + ) + + // Optimistically update the cache + if (previousCard) { + const { expiryDate, ...safeInput } = input + queryClient.setQueryData(cardKeys.detail(input.id), { + ...previousCard, + ...safeInput, + ...(expiryDate && { expiryDate: new Date(expiryDate) }), + }) + } + + return { previousCard } + }, + onError: (error, input, context) => { + // Rollback the cache to the previous value + if (context?.previousCard) { + queryClient.setQueryData( + cardKeys.detail(input.id), + context.previousCard + ) + } + console.error("Failed to update card:", error) + }, + onSuccess: (updatedCard: CardOutput) => { + // Update the cache with the server response + queryClient.setQueryData(cardKeys.detail(updatedCard.id), updatedCard) + + // Invalidate related queries + queryClient.invalidateQueries({ queryKey: cardKeys.lists() }) + }, + }) +} + +// Delete card mutation +export function useDeleteCard() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: DeleteCardInput) => orpc.cards.delete.call(input), + onMutate: async (input) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: cardKeys.detail(input.id), + }) + + // Snapshot the previous value + const previousCard = queryClient.getQueryData( + cardKeys.detail(input.id) + ) + + // Optimistically remove from cache + queryClient.removeQueries({ + queryKey: cardKeys.detail(input.id), + }) + + return { previousCard } + }, + onError: (error, input, context) => { + // Restore the cache if deletion failed + if (context?.previousCard) { + queryClient.setQueryData( + cardKeys.detail(input.id), + context.previousCard + ) + } + console.error("Failed to delete card:", error) + }, + onSuccess: () => { + // Invalidate and refetch card lists + queryClient.invalidateQueries({ queryKey: cardKeys.lists() }) + }, + }) +} diff --git a/orpc/hooks/use-containers.ts b/orpc/hooks/use-containers.ts new file mode 100644 index 0000000..ec74fdc --- /dev/null +++ b/orpc/hooks/use-containers.ts @@ -0,0 +1,189 @@ +"use client" + +import { orpc } from "@/orpc/client" +import type { + ContainerOutput, + CreateContainerInput, + DeleteContainerInput, + ListContainersInput, + UpdateContainerInput, +} from "@/schemas/utils/dto" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" + +// Query keys factory +export const containerKeys = { + all: ["containers"] as const, + lists: () => [...containerKeys.all, "list"] as const, + list: (filters: Partial) => + [...containerKeys.lists(), filters] as const, + details: () => [...containerKeys.all, "detail"] as const, + detail: (id: string) => [...containerKeys.details(), id] as const, +} + +// Get single container +export function useContainer(id: string) { + return useQuery({ + queryKey: containerKeys.detail(id), + queryFn: () => orpc.containers.get.call({ id }), + enabled: !!id, + }) +} + +// List containers with pagination +export function useContainers( + input: ListContainersInput = { page: 1, limit: 10 } +) { + return useQuery({ + queryKey: containerKeys.list(input), + queryFn: () => orpc.containers.list.call(input), + placeholderData: (previousData) => previousData, + }) +} + +// Create container mutation +export function useCreateContainer() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: CreateContainerInput) => + orpc.containers.create.call(input), + onSuccess: (newContainer: ContainerOutput) => { + // Invalidate and refetch container lists + queryClient.invalidateQueries({ queryKey: containerKeys.lists() }) + + // Add the new container to the cache + queryClient.setQueryData( + containerKeys.detail(newContainer.id), + newContainer + ) + }, + onError: (error) => { + console.error("Failed to create container:", error) + }, + }) +} + +// Create container with secrets mutation +export function useCreateContainerWithSecrets() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: orpc.containers.createWithSecrets.call, + onSuccess: (result) => { + if (result.success && result.container) { + // Invalidate related queries + queryClient.invalidateQueries({ + queryKey: containerKeys.lists(), + }) + queryClient.invalidateQueries({ queryKey: ["secrets", "list"] }) + + // Add the new container to the cache if available + if (result.container) { + queryClient.setQueryData( + containerKeys.detail(result.container.id), + result.container + ) + } + } + }, + onError: (error) => { + console.error("Failed to create container with secrets:", error) + }, + }) +} + +// Update container mutation +export function useUpdateContainer() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: UpdateContainerInput) => + orpc.containers.update.call(input), + onMutate: async (input) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: containerKeys.detail(input.id), + }) + + // Snapshot the previous value + const previousContainer = queryClient.getQueryData( + containerKeys.detail(input.id) + ) + + // Optimistically update the cache + if (previousContainer) { + queryClient.setQueryData( + containerKeys.detail(input.id), + { + ...previousContainer, + ...input, + } + ) + } + + return { previousContainer } + }, + onError: (error, input, context) => { + // Rollback the cache to the previous value + if (context?.previousContainer) { + queryClient.setQueryData( + containerKeys.detail(input.id), + context.previousContainer + ) + } + console.error("Failed to update container:", error) + }, + onSuccess: (updatedContainer: ContainerOutput) => { + // Update the cache with the server response + queryClient.setQueryData( + containerKeys.detail(updatedContainer.id), + updatedContainer + ) + + // Invalidate related queries + queryClient.invalidateQueries({ queryKey: containerKeys.lists() }) + }, + }) +} + +// Delete container mutation +export function useDeleteContainer() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: DeleteContainerInput) => + orpc.containers.delete.call(input), + onMutate: async (input) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: containerKeys.detail(input.id), + }) + + // Snapshot the previous value + const previousContainer = queryClient.getQueryData( + containerKeys.detail(input.id) + ) + + // Optimistically remove from cache + queryClient.removeQueries({ + queryKey: containerKeys.detail(input.id), + }) + + return { previousContainer } + }, + onError: (error, input, context) => { + // Restore the cache if deletion failed + if (context?.previousContainer) { + queryClient.setQueryData( + containerKeys.detail(input.id), + context.previousContainer + ) + } + console.error("Failed to delete container:", error) + }, + onSuccess: () => { + // Invalidate and refetch container lists + queryClient.invalidateQueries({ queryKey: containerKeys.lists() }) + }, + }) +} diff --git a/orpc/hooks/use-credentials.ts b/orpc/hooks/use-credentials.ts new file mode 100644 index 0000000..7bd0adc --- /dev/null +++ b/orpc/hooks/use-credentials.ts @@ -0,0 +1,186 @@ +"use client" + +import { orpc } from "@/orpc/client" +import type { + CreateCredentialInput, + CredentialOutput, + DeleteCredentialInput, + ListCredentialsInput, + UpdateCredentialInput, +} from "@/schemas/credential/dto" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" + +// Query keys factory +export const credentialKeys = { + all: ["credentials"] as const, + lists: () => [...credentialKeys.all, "list"] as const, + list: (filters: Partial) => + [...credentialKeys.lists(), filters] as const, + details: () => [...credentialKeys.all, "detail"] as const, + detail: (id: string) => [...credentialKeys.details(), id] as const, +} + +// Get single credential +export function useCredential(id: string) { + return useQuery({ + queryKey: credentialKeys.detail(id), + queryFn: () => orpc.credentials.get.call({ id }), + enabled: !!id, + }) +} + +// List credentials with pagination +export function useCredentials( + input: ListCredentialsInput = { page: 1, limit: 10 } +) { + return useQuery({ + queryKey: credentialKeys.list(input), + queryFn: () => orpc.credentials.list.call(input), + placeholderData: (previousData) => previousData, + }) +} + +// Create credential mutation +export function useCreateCredential() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: CreateCredentialInput) => + orpc.credentials.create.call(input), + onSuccess: (newCredential: CredentialOutput) => { + // Invalidate and refetch credential lists + queryClient.invalidateQueries({ queryKey: credentialKeys.lists() }) + + // Add the new credential to the cache + queryClient.setQueryData( + credentialKeys.detail(newCredential.id), + newCredential + ) + }, + onError: (error) => { + console.error("Failed to create credential:", error) + }, + }) +} + +// Create credential with metadata mutation +export function useCreateCredentialWithMetadata() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: orpc.credentials.createWithMetadata.call, + onSuccess: (result) => { + if (result.success && result.credential) { + // Invalidate and refetch credential lists + queryClient.invalidateQueries({ + queryKey: credentialKeys.lists(), + }) + + // Add the new credential to the cache + queryClient.setQueryData( + credentialKeys.detail(result.credential.id), + result.credential + ) + } + }, + onError: (error) => { + console.error("Failed to create credential with metadata:", error) + }, + }) +} + +// Update credential mutation +export function useUpdateCredential() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: UpdateCredentialInput) => + orpc.credentials.update.call(input), + onMutate: async (input) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: credentialKeys.detail(input.id), + }) + + // Snapshot the previous value + const previousCredential = queryClient.getQueryData( + credentialKeys.detail(input.id) + ) + + // Optimistically update the cache + if (previousCredential) { + queryClient.setQueryData( + credentialKeys.detail(input.id), + { + ...previousCredential, + ...input, + } + ) + } + + return { previousCredential } + }, + onError: (error, input, context) => { + // Rollback the cache to the previous value + if (context?.previousCredential) { + queryClient.setQueryData( + credentialKeys.detail(input.id), + context.previousCredential + ) + } + console.error("Failed to update credential:", error) + }, + onSuccess: (updatedCredential: CredentialOutput) => { + // Update the cache with the server response + queryClient.setQueryData( + credentialKeys.detail(updatedCredential.id), + updatedCredential + ) + + // Invalidate related queries + queryClient.invalidateQueries({ queryKey: credentialKeys.lists() }) + }, + }) +} + +// Delete credential mutation +export function useDeleteCredential() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: DeleteCredentialInput) => + orpc.credentials.delete.call(input), + onMutate: async (input) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: credentialKeys.detail(input.id), + }) + + // Snapshot the previous value + const previousCredential = queryClient.getQueryData( + credentialKeys.detail(input.id) + ) + + // Optimistically remove from cache + queryClient.removeQueries({ + queryKey: credentialKeys.detail(input.id), + }) + + return { previousCredential } + }, + onError: (error, input, context) => { + // Restore the cache if deletion failed + if (context?.previousCredential) { + queryClient.setQueryData( + credentialKeys.detail(input.id), + context.previousCredential + ) + } + console.error("Failed to delete credential:", error) + }, + onSuccess: () => { + // Invalidate and refetch credential lists + queryClient.invalidateQueries({ queryKey: credentialKeys.lists() }) + }, + }) +} diff --git a/orpc/hooks/use-platforms.ts b/orpc/hooks/use-platforms.ts new file mode 100644 index 0000000..a0e7396 --- /dev/null +++ b/orpc/hooks/use-platforms.ts @@ -0,0 +1,24 @@ +"use client" + +import { orpc } from "@/orpc/client" +import type { ListPlatformsInput } from "@/schemas/utils/dto" +import { useQuery } from "@tanstack/react-query" + +// Query keys factory +export const platformKeys = { + all: ["platforms"] as const, + lists: () => [...platformKeys.all, "list"] as const, + list: (filters: Partial) => + [...platformKeys.lists(), filters] as const, +} + +// List platforms with pagination +export function usePlatforms( + input: ListPlatformsInput = { page: 1, limit: 100 } +) { + return useQuery({ + queryKey: platformKeys.list(input), + queryFn: () => orpc.platforms.list.call(input), + placeholderData: (previousData) => previousData, + }) +} diff --git a/orpc/hooks/use-secrets.ts b/orpc/hooks/use-secrets.ts new file mode 100644 index 0000000..5fec292 --- /dev/null +++ b/orpc/hooks/use-secrets.ts @@ -0,0 +1,149 @@ +"use client" + +import { orpc } from "@/orpc/client" +import type { + CreateSecretInput, + DeleteSecretInput, + ListSecretsInput, + SecretOutput, + UpdateSecretInput, +} from "@/schemas/secrets/dto" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" + +// Query keys factory +export const secretKeys = { + all: ["secrets"] as const, + lists: () => [...secretKeys.all, "list"] as const, + list: (filters: Partial) => + [...secretKeys.lists(), filters] as const, + details: () => [...secretKeys.all, "detail"] as const, + detail: (id: string) => [...secretKeys.details(), id] as const, +} + +// Get single secret +export function useSecret(id: string) { + return useQuery({ + queryKey: secretKeys.detail(id), + queryFn: () => orpc.secrets.get.call({ id }), + enabled: !!id, + }) +} + +// List secrets with pagination +export function useSecrets(input: ListSecretsInput = { page: 1, limit: 10 }) { + return useQuery({ + queryKey: secretKeys.list(input), + queryFn: () => orpc.secrets.list.call(input), + placeholderData: (previousData) => previousData, + }) +} + +// Create secret mutation +export function useCreateSecret() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: CreateSecretInput) => orpc.secrets.create.call(input), + onSuccess: (newSecret: SecretOutput) => { + // Invalidate and refetch secret lists + queryClient.invalidateQueries({ queryKey: secretKeys.lists() }) + + // Add the new secret to the cache + queryClient.setQueryData(secretKeys.detail(newSecret.id), newSecret) + }, + onError: (error) => { + console.error("Failed to create secret:", error) + }, + }) +} + +// Update secret mutation +export function useUpdateSecret() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: UpdateSecretInput) => orpc.secrets.update.call(input), + onMutate: async (input) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: secretKeys.detail(input.id), + }) + + // Snapshot the previous value + const previousSecret = queryClient.getQueryData( + secretKeys.detail(input.id) + ) + + // Optimistically update the cache + if (previousSecret) { + queryClient.setQueryData(secretKeys.detail(input.id), { + ...previousSecret, + ...input, + }) + } + + return { previousSecret } + }, + onError: (error, input, context) => { + // Rollback the cache to the previous value + if (context?.previousSecret) { + queryClient.setQueryData( + secretKeys.detail(input.id), + context.previousSecret + ) + } + console.error("Failed to update secret:", error) + }, + onSuccess: (updatedSecret: SecretOutput) => { + // Update the cache with the server response + queryClient.setQueryData( + secretKeys.detail(updatedSecret.id), + updatedSecret + ) + + // Invalidate related queries + queryClient.invalidateQueries({ queryKey: secretKeys.lists() }) + }, + }) +} + +// Delete secret mutation +export function useDeleteSecret() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (input: DeleteSecretInput) => orpc.secrets.delete.call(input), + onMutate: async (input) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: secretKeys.detail(input.id), + }) + + // Snapshot the previous value + const previousSecret = queryClient.getQueryData( + secretKeys.detail(input.id) + ) + + // Optimistically remove from cache + queryClient.removeQueries({ + queryKey: secretKeys.detail(input.id), + }) + + return { previousSecret } + }, + onError: (error, input, context) => { + // Restore the cache if deletion failed + if (context?.previousSecret) { + queryClient.setQueryData( + secretKeys.detail(input.id), + context.previousSecret + ) + } + console.error("Failed to delete secret:", error) + }, + onSuccess: () => { + // Invalidate and refetch secret lists + queryClient.invalidateQueries({ queryKey: secretKeys.lists() }) + }, + }) +} diff --git a/orpc/hooks/use-tags.ts b/orpc/hooks/use-tags.ts new file mode 100644 index 0000000..0dfbab9 --- /dev/null +++ b/orpc/hooks/use-tags.ts @@ -0,0 +1,22 @@ +"use client" + +import { orpc } from "@/orpc/client" +import type { ListTagsInput } from "@/schemas/utils/dto" +import { useQuery } from "@tanstack/react-query" + +// Query keys factory +export const tagKeys = { + all: ["tags"] as const, + lists: () => [...tagKeys.all, "list"] as const, + list: (filters: Partial) => + [...tagKeys.lists(), filters] as const, +} + +// List tags with pagination +export function useTags(input: ListTagsInput = { page: 1, limit: 100 }) { + return useQuery({ + queryKey: tagKeys.list(input), + queryFn: () => orpc.tags.list.call(input), + placeholderData: (previousData) => previousData, + }) +} diff --git a/orpc/hooks/use-users.ts b/orpc/hooks/use-users.ts new file mode 100644 index 0000000..6bc23fd --- /dev/null +++ b/orpc/hooks/use-users.ts @@ -0,0 +1,59 @@ +"use client" + +import { orpc } from "@/orpc/client" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" + +// Query keys factory +export const userKeys = { + all: ["users"] as const, + waitlistCount: () => [...userKeys.all, "waitlistCount"] as const, + userCount: () => [...userKeys.all, "userCount"] as const, + encryptedDataCount: () => [...userKeys.all, "encryptedDataCount"] as const, +} + +// Join waitlist mutation +export function useJoinWaitlist() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: orpc.users.joinWaitlist.call, + onSuccess: (data) => { + if (data.success) { + // Invalidate waitlist count to refetch + queryClient.invalidateQueries({ + queryKey: userKeys.waitlistCount(), + }) + } + }, + onError: (error) => { + console.error("Failed to join waitlist:", error) + }, + }) +} + +// Get waitlist count +export function useWaitlistCount() { + return useQuery({ + queryKey: userKeys.waitlistCount(), + queryFn: () => orpc.users.getWaitlistCount.call({}), + staleTime: 5 * 60 * 1000, // 5 minutes + }) +} + +// Get user count +export function useUserCount() { + return useQuery({ + queryKey: userKeys.userCount(), + queryFn: () => orpc.users.getUserCount.call({}), + staleTime: 5 * 60 * 1000, // 5 minutes + }) +} + +// Get encrypted data count +export function useEncryptedDataCount() { + return useQuery({ + queryKey: userKeys.encryptedDataCount(), + queryFn: () => orpc.users.getEncryptedDataCount.call({}), + staleTime: 5 * 60 * 1000, // 5 minutes + }) +} diff --git a/orpc/routers/card.ts b/orpc/routers/card.ts new file mode 100644 index 0000000..c8cf792 --- /dev/null +++ b/orpc/routers/card.ts @@ -0,0 +1,286 @@ +import { CardEntity } from "@/entities/card/card" +import { authMiddleware } from "@/middleware/auth" +import { database } from "@/prisma/client" +import { + cardOutputSchema, + createCardInputSchema, + deleteCardInputSchema, + getCardInputSchema, + listCardsInputSchema, + listCardsOutputSchema, + updateCardInputSchema, + type CardOutput, + type ListCardsOutput, +} from "@/schemas/card/dto" +import { ORPCError, os } from "@orpc/server" +import type { Prisma } from "@prisma/client" + +import { getOrReturnEmptyObject } from "@/lib/utils" +import { CardExpiryDateUtils } from "@/lib/utils/card-expiry-helpers" +import { createEncryptedData } from "@/lib/utils/encryption-helpers" +import { createTagsAndGetConnections } from "@/lib/utils/tag-helpers" + +import type { ORPCContext } from "../types" + +const baseProcedure = os.$context() +const authProcedure = baseProcedure.use(({ context, next }) => + authMiddleware({ context, next }) +) + +// Get card by ID +export const getCard = authProcedure + .input(getCardInputSchema) + .output(cardOutputSchema) + .handler(async ({ input, context }): Promise => { + const card = await database.card.findFirst({ + where: { + id: input.id, + userId: context.user.id, + }, + }) + + if (!card) { + throw new ORPCError("NOT_FOUND") + } + + return CardEntity.getSimpleRo(card) + }) + +// List cards with pagination +export const listCards = authProcedure + .input(listCardsInputSchema) + .output(listCardsOutputSchema) + .handler(async ({ input, context }): Promise => { + const { page, limit, search, containerId } = input + const skip = (page - 1) * limit + + const where = { + userId: context.user.id, + ...(containerId && { containerId }), + ...(search && { + OR: [ + { name: { contains: search, mode: "insensitive" as const } }, + { description: { contains: search, mode: "insensitive" as const } }, + { + cardholderName: { contains: search, mode: "insensitive" as const }, + }, + ], + }), + } + + const [cards, total] = await Promise.all([ + database.card.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: "desc" }, + }), + database.card.count({ where }), + ]) + + return { + cards: cards.map((card) => CardEntity.getSimpleRo(card)), + total, + hasMore: skip + cards.length < total, + page, + limit, + } + }) + +// Create card +export const createCard = authProcedure + .input(createCardInputSchema) + .output(cardOutputSchema) + .handler(async ({ input, context }): Promise => { + // Handle expiry date using shared utility + const expiryDate = CardExpiryDateUtils.processServerExpiryDate( + input.expiryDate + ) + + const tagConnections = await createTagsAndGetConnections( + input.tags, + context.user.id, + input.containerId + ) + + // Create encrypted data for CVV + const cvvEncryptionResult = await createEncryptedData({ + encryptedValue: input.cvvEncryption.encryptedValue, + encryptionKey: input.cvvEncryption.encryptionKey, + iv: input.cvvEncryption.iv, + }) + + if (!cvvEncryptionResult.success || !cvvEncryptionResult.encryptedData) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + // Create encrypted data for card number + const numberEncryptionResult = await createEncryptedData({ + encryptedValue: input.numberEncryption.encryptedValue, + encryptionKey: input.numberEncryption.encryptionKey, + iv: input.numberEncryption.iv, + }) + + if ( + !numberEncryptionResult.success || + !numberEncryptionResult.encryptedData + ) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + const card = await database.card.create({ + data: { + name: input.name, + description: input.description, + type: input.type, + provider: input.provider, + status: input.status, + expiryDate, + billingAddress: input.billingAddress, + cardholderName: input.cardholderName, + cardholderEmail: input.cardholderEmail, + userId: context.user.id, + tags: tagConnections, + cvvEncryptionId: cvvEncryptionResult.encryptedData.id, + numberEncryptionId: numberEncryptionResult.encryptedData.id, + ...getOrReturnEmptyObject(input.containerId, "containerId"), + }, + }) + + return CardEntity.getSimpleRo(card) + }) + +// Update card +export const updateCard = authProcedure + .input(updateCardInputSchema) + .output(cardOutputSchema) + .handler(async ({ input, context }): Promise => { + const { id, ...updateData } = input + + // Verify card ownership + const existingCard = await database.card.findFirst({ + where: { + id, + userId: context.user.id, + }, + }) + + if (!existingCard) { + throw new ORPCError("NOT_FOUND") + } + + // Process the update data + const updatePayload: Prisma.CardUpdateInput = {} + + if (updateData.name !== undefined) updatePayload.name = updateData.name + if (updateData.description !== undefined) + updatePayload.description = updateData.description + if (updateData.type !== undefined) updatePayload.type = updateData.type + if (updateData.provider !== undefined) + updatePayload.provider = updateData.provider + if (updateData.status !== undefined) + updatePayload.status = updateData.status + if (updateData.cardholderName !== undefined) + updatePayload.cardholderName = updateData.cardholderName + if (updateData.billingAddress !== undefined) + updatePayload.billingAddress = updateData.billingAddress + if (updateData.cardholderEmail !== undefined) + updatePayload.cardholderEmail = updateData.cardholderEmail + if (updateData.containerId !== undefined) + updatePayload.container = updateData.containerId + ? { connect: { id: updateData.containerId } } + : { disconnect: true } + + // Handle expiry date if provided + if (updateData.expiryDate !== undefined) { + updatePayload.expiryDate = CardExpiryDateUtils.processServerExpiryDate( + updateData.expiryDate + ) + } + + // Handle tags if provided + if (updateData.tags !== undefined) { + const tagConnections = await createTagsAndGetConnections( + updateData.tags, + context.user.id, + updateData.containerId || existingCard.containerId || undefined + ) + updatePayload.tags = tagConnections + } + + // Handle encryption updates if provided + if (updateData.cvvEncryption) { + const cvvEncryptionResult = await createEncryptedData({ + encryptedValue: updateData.cvvEncryption.encryptedValue, + encryptionKey: updateData.cvvEncryption.encryptionKey, + iv: updateData.cvvEncryption.iv, + }) + + if (!cvvEncryptionResult.success || !cvvEncryptionResult.encryptedData) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + updatePayload.cvvEncryption = { + connect: { id: cvvEncryptionResult.encryptedData.id }, + } + } + + if (updateData.numberEncryption) { + const numberEncryptionResult = await createEncryptedData({ + encryptedValue: updateData.numberEncryption.encryptedValue, + encryptionKey: updateData.numberEncryption.encryptionKey, + iv: updateData.numberEncryption.iv, + }) + + if ( + !numberEncryptionResult.success || + !numberEncryptionResult.encryptedData + ) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + updatePayload.numberEncryption = { + connect: { id: numberEncryptionResult.encryptedData.id }, + } + } + + const updatedCard = await database.card.update({ + where: { id }, + data: updatePayload, + }) + + return CardEntity.getSimpleRo(updatedCard) + }) + +// Delete card +export const deleteCard = authProcedure + .input(deleteCardInputSchema) + .output(cardOutputSchema) + .handler(async ({ input, context }): Promise => { + // Verify card ownership + const existingCard = await database.card.findFirst({ + where: { + id: input.id, + userId: context.user.id, + }, + }) + + if (!existingCard) { + throw new ORPCError("NOT_FOUND") + } + + const deletedCard = await database.card.delete({ + where: { id: input.id }, + }) + + return CardEntity.getSimpleRo(deletedCard) + }) + +// Export the card router +export const cardRouter = { + get: getCard, + list: listCards, + create: createCard, + update: updateCard, + delete: deleteCard, +} diff --git a/orpc/routers/container.ts b/orpc/routers/container.ts new file mode 100644 index 0000000..6a6afce --- /dev/null +++ b/orpc/routers/container.ts @@ -0,0 +1,286 @@ +import { ContainerEntity } from "@/entities/utils/container/entity" +import { authMiddleware } from "@/middleware/auth" +import { database } from "@/prisma/client" +import { + createContainerWithSecretsInputSchema, + createContainerWithSecretsOutputSchema, + type CreateContainerWithSecretsInput, + type CreateContainerWithSecretsOutput, +} from "@/schemas/utils/container-with-secrets" +import { + containerOutputSchema, + createContainerInputSchema, + deleteContainerInputSchema, + getContainerInputSchema, + listContainersInputSchema, + listContainersOutputSchema, + updateContainerInputSchema, + type ContainerOutput, + type ListContainersOutput, +} from "@/schemas/utils/dto" +import { ORPCError, os } from "@orpc/server" +import type { Prisma } from "@prisma/client" + +import { createEncryptedData } from "@/lib/utils/encryption-helpers" +import { createTagsAndGetConnections } from "@/lib/utils/tag-helpers" + +import type { ORPCContext } from "../types" + +const baseProcedure = os.$context() +const authProcedure = baseProcedure.use(({ context, next }) => + authMiddleware({ context, next }) +) + +// Get container by ID +export const getContainer = authProcedure + .input(getContainerInputSchema) + .output(containerOutputSchema) + .handler(async ({ input, context }): Promise => { + const container = await database.container.findFirst({ + where: { + id: input.id, + userId: context.user.id, + }, + }) + + if (!container) { + throw new ORPCError("NOT_FOUND") + } + + return ContainerEntity.getSimpleRo(container) + }) + +// List containers with pagination +export const listContainers = authProcedure + .input(listContainersInputSchema) + .output(listContainersOutputSchema) + .handler(async ({ input, context }): Promise => { + const { page, limit, search } = input + const skip = (page - 1) * limit + + const where = { + userId: context.user.id, + ...(search && { + OR: [ + { name: { contains: search, mode: "insensitive" as const } }, + { description: { contains: search, mode: "insensitive" as const } }, + ], + }), + } + + const [containers, total] = await Promise.all([ + database.container.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: "desc" }, + }), + database.container.count({ where }), + ]) + + return { + containers: containers.map((container) => + ContainerEntity.getSimpleRo(container) + ), + total, + hasMore: skip + containers.length < total, + page, + limit, + } + }) + +// Create container +export const createContainer = authProcedure + .input(createContainerInputSchema) + .output(containerOutputSchema) + .handler(async ({ input, context }): Promise => { + const tagConnections = await createTagsAndGetConnections( + input.tags, + context.user.id, + undefined + ) + + const container = await database.container.create({ + data: { + name: input.name, + icon: input.icon, + description: input.description, + type: input.type, + userId: context.user.id, + tags: tagConnections, + }, + }) + + return ContainerEntity.getSimpleRo(container) + }) + +// Update container +export const updateContainer = authProcedure + .input(updateContainerInputSchema) + .output(containerOutputSchema) + .handler(async ({ input, context }): Promise => { + const { id, ...updateData } = input + + // Verify container ownership + const existingContainer = await database.container.findFirst({ + where: { + id, + userId: context.user.id, + }, + }) + + if (!existingContainer) { + throw new ORPCError("NOT_FOUND") + } + + // Process the update data + const updatePayload: Prisma.ContainerUpdateInput = {} + + if (updateData.name !== undefined) updatePayload.name = updateData.name + if (updateData.icon !== undefined) updatePayload.icon = updateData.icon + if (updateData.description !== undefined) + updatePayload.description = updateData.description + if (updateData.type !== undefined) updatePayload.type = updateData.type + + // Handle tags if provided + if (updateData.tags !== undefined) { + const tagConnections = await createTagsAndGetConnections( + updateData.tags, + context.user.id, + undefined + ) + updatePayload.tags = tagConnections + } + + const updatedContainer = await database.container.update({ + where: { id }, + data: updatePayload, + }) + + return ContainerEntity.getSimpleRo(updatedContainer) + }) + +// Delete container +export const deleteContainer = authProcedure + .input(deleteContainerInputSchema) + .output(containerOutputSchema) + .handler(async ({ input, context }): Promise => { + // Verify container ownership + const existingContainer = await database.container.findFirst({ + where: { + id: input.id, + userId: context.user.id, + }, + }) + + if (!existingContainer) { + throw new ORPCError("NOT_FOUND") + } + + const deletedContainer = await database.container.delete({ + where: { id: input.id }, + }) + + return ContainerEntity.getSimpleRo(deletedContainer) + }) + +// Create container with secrets +export const createContainerWithSecrets = authProcedure + .input(createContainerWithSecretsInputSchema) + .output(createContainerWithSecretsOutputSchema) + .handler( + async ({ input, context }): Promise => { + const { container: containerData, secrets: secretsData } = input + + try { + const result = await database.$transaction(async (tx) => { + // Create container with tags + const tagConnections = await createTagsAndGetConnections( + containerData.tags, + context.user.id, + undefined + ) + + const container = await tx.container.create({ + data: { + name: containerData.name, + icon: containerData.icon, + description: containerData.description, + type: containerData.type, + userId: context.user.id, + tags: tagConnections, + }, + }) + + // Create encrypted data and secrets + const createdSecrets = [] + for (const secretData of secretsData) { + // Use the helper function for consistency + const encryptionResult = await createEncryptedData({ + iv: secretData.valueEncryption.iv, + encryptedValue: secretData.valueEncryption.encryptedValue, + encryptionKey: secretData.valueEncryption.encryptionKey, + }) + + if (!encryptionResult.success || !encryptionResult.encryptedData) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + // Create secret + const secret = await tx.secret.create({ + data: { + name: secretData.name, + note: secretData.note, + userId: context.user.id, + containerId: container.id, + valueEncryptionId: encryptionResult.encryptedData.id, + }, + }) + + createdSecrets.push(secret) + } + + return { container, createdSecrets } + }) + + return { + success: true, + container: ContainerEntity.getSimpleRo(result.container), + secrets: result.createdSecrets.map((secret) => ({ + id: secret.id, + name: secret.name, + note: secret.note, + lastViewed: secret.lastViewed, + updatedAt: secret.updatedAt, + createdAt: secret.createdAt, + userId: secret.userId, + containerId: secret.containerId, + valueEncryptionId: secret.valueEncryptionId, + })), + } + } catch (error) { + console.error("Error creating container with secrets:", error) + + // If it's an ORPCError, re-throw it to maintain consistent error handling + if (error instanceof ORPCError) { + throw error + } + + return { + success: false, + error: + error instanceof Error ? error.message : "Unknown error occurred", + } + } + } + ) + +// Export the container router +export const containerRouter = { + get: getContainer, + list: listContainers, + create: createContainer, + update: updateContainer, + delete: deleteContainer, + createWithSecrets: createContainerWithSecrets, +} diff --git a/orpc/routers/credential.ts b/orpc/routers/credential.ts new file mode 100644 index 0000000..465abb4 --- /dev/null +++ b/orpc/routers/credential.ts @@ -0,0 +1,351 @@ +import { CredentialEntity } from "@/entities/credential/credential" +import { authMiddleware } from "@/middleware/auth" +import { database } from "@/prisma/client" +import { + createCredentialWithMetadataInputSchema, + createCredentialWithMetadataOutputSchema, + type CreateCredentialWithMetadataInput, + type CreateCredentialWithMetadataOutput, +} from "@/schemas/credential/credential-with-metadata" +import { + createCredentialInputSchema, + credentialOutputSchema, + deleteCredentialInputSchema, + getCredentialInputSchema, + listCredentialsInputSchema, + listCredentialsOutputSchema, + updateCredentialInputSchema, + type CredentialOutput, + type ListCredentialsOutput, +} from "@/schemas/credential/dto" +import { ORPCError, os } from "@orpc/server" +import type { Prisma } from "@prisma/client" +import { z } from "zod" + +import { getOrReturnEmptyObject } from "@/lib/utils" +import { createEncryptedData } from "@/lib/utils/encryption-helpers" +import { createTagsAndGetConnections } from "@/lib/utils/tag-helpers" + +import type { ORPCContext } from "../types" + +const baseProcedure = os.$context() +const authProcedure = baseProcedure.use(({ context, next }) => + authMiddleware({ context, next }) +) + +// Get credential by ID +export const getCredential = authProcedure + .input(getCredentialInputSchema) + .output(credentialOutputSchema) + .handler(async ({ input, context }): Promise => { + const credential = await database.credential.findFirst({ + where: { + id: input.id, + userId: context.user.id, + }, + }) + + if (!credential) { + throw new ORPCError("NOT_FOUND") + } + + // Update last viewed timestamp + await database.credential.update({ + where: { id: input.id }, + data: { lastViewed: new Date() }, + }) + + return CredentialEntity.getSimpleRo(credential) + }) + +// List credentials with pagination +export const listCredentials = authProcedure + .input(listCredentialsInputSchema) + .output(listCredentialsOutputSchema) + .handler(async ({ input, context }): Promise => { + const { page, limit, search, containerId, platformId } = input + const skip = (page - 1) * limit + + const where = { + userId: context.user.id, + ...(containerId && { containerId }), + ...(platformId && { platformId }), + ...(search && { + OR: [ + { identifier: { contains: search, mode: "insensitive" as const } }, + { description: { contains: search, mode: "insensitive" as const } }, + ], + }), + } + + const [credentials, total] = await Promise.all([ + database.credential.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: "desc" }, + }), + database.credential.count({ where }), + ]) + + return { + credentials: credentials.map((credential) => + CredentialEntity.getSimpleRo(credential) + ), + total, + hasMore: skip + credentials.length < total, + page, + limit, + } + }) + +// Create credential +export const createCredential = authProcedure + .input(createCredentialInputSchema) + .output(credentialOutputSchema) + .handler(async ({ input, context }): Promise => { + // Verify platform exists + const platform = await database.platform.findUnique({ + where: { id: input.platformId }, + }) + + if (!platform) { + throw new ORPCError("NOT_FOUND") + } + + const tagConnections = await createTagsAndGetConnections( + input.tags, + context.user.id, + input.containerId + ) + + // Create encrypted data for password + const passwordEncryptionResult = await createEncryptedData({ + encryptedValue: input.passwordEncryption.encryptedValue, + encryptionKey: input.passwordEncryption.encryptionKey, + iv: input.passwordEncryption.iv, + }) + + if ( + !passwordEncryptionResult.success || + !passwordEncryptionResult.encryptedData + ) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + const credential = await database.credential.create({ + data: { + identifier: input.identifier, + passwordEncryptionId: passwordEncryptionResult.encryptedData.id, + status: input.status, + platformId: input.platformId, + description: input.description, + userId: context.user.id, + tags: tagConnections, + ...getOrReturnEmptyObject(input.containerId, "containerId"), + }, + }) + + return CredentialEntity.getSimpleRo(credential) + }) + +// Update credential +export const updateCredential = authProcedure + .input(updateCredentialInputSchema) + .output(credentialOutputSchema) + .handler(async ({ input, context }): Promise => { + const { id, ...updateData } = input + + // Verify credential ownership + const existingCredential = await database.credential.findFirst({ + where: { + id, + userId: context.user.id, + }, + }) + + if (!existingCredential) { + throw new ORPCError("NOT_FOUND") + } + + // Process the update data + const updatePayload: Prisma.CredentialUpdateInput = {} + + if (updateData.identifier !== undefined) + updatePayload.identifier = updateData.identifier + if (updateData.description !== undefined) + updatePayload.description = updateData.description + if (updateData.status !== undefined) + updatePayload.status = updateData.status + if (updateData.platformId !== undefined) + updatePayload.platform = { connect: { id: updateData.platformId } } + if (updateData.containerId !== undefined) { + updatePayload.container = updateData.containerId + ? { connect: { id: updateData.containerId } } + : { disconnect: true } + } + + // Handle tags if provided + if (updateData.tags !== undefined) { + const tagConnections = await createTagsAndGetConnections( + updateData.tags, + context.user.id, + updateData.containerId || existingCredential.containerId || undefined + ) + updatePayload.tags = tagConnections + } + + // Handle password encryption updates if provided + if (updateData.passwordEncryption) { + const passwordEncryptionResult = await createEncryptedData({ + encryptedValue: updateData.passwordEncryption.encryptedValue, + encryptionKey: updateData.passwordEncryption.encryptionKey, + iv: updateData.passwordEncryption.iv, + }) + + if ( + !passwordEncryptionResult.success || + !passwordEncryptionResult.encryptedData + ) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + updatePayload.passwordEncryption = { + connect: { id: passwordEncryptionResult.encryptedData.id }, + } + } + + const updatedCredential = await database.credential.update({ + where: { id }, + data: updatePayload, + }) + + return CredentialEntity.getSimpleRo(updatedCredential) + }) + +// Delete credential +export const deleteCredential = authProcedure + .input(deleteCredentialInputSchema) + .output(credentialOutputSchema) + .handler(async ({ input, context }): Promise => { + // Verify credential ownership + const existingCredential = await database.credential.findFirst({ + where: { + id: input.id, + userId: context.user.id, + }, + }) + + if (!existingCredential) { + throw new ORPCError("NOT_FOUND") + } + + const deletedCredential = await database.credential.delete({ + where: { id: input.id }, + }) + + return CredentialEntity.getSimpleRo(deletedCredential) + }) + +// Create credential with metadata +export const createCredentialWithMetadata = authProcedure + .input(createCredentialWithMetadataInputSchema) + .output(createCredentialWithMetadataOutputSchema) + .handler( + async ({ input, context }): Promise => { + const { credential: credentialData, metadata } = input + + try { + // Verify platform exists + const platform = await database.platform.findUnique({ + where: { id: credentialData.platformId }, + }) + + if (!platform) { + throw new ORPCError("NOT_FOUND") + } + + const tagConnections = await createTagsAndGetConnections( + credentialData.tags, + context.user.id, + credentialData.containerId + ) + + // Create encrypted data for password + const passwordEncryptionResult = await createEncryptedData({ + encryptedValue: credentialData.passwordEncryption.encryptedValue, + encryptionKey: credentialData.passwordEncryption.encryptionKey, + iv: credentialData.passwordEncryption.iv, + }) + + if ( + !passwordEncryptionResult.success || + !passwordEncryptionResult.encryptedData + ) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + // Use transaction for atomicity + const result = await database.$transaction(async (tx) => { + const credential = await tx.credential.create({ + data: { + identifier: credentialData.identifier, + passwordEncryptionId: passwordEncryptionResult.encryptedData!.id, + status: credentialData.status, + platformId: credentialData.platformId, + description: credentialData.description, + userId: context.user.id, + tags: tagConnections, + ...getOrReturnEmptyObject( + credentialData.containerId, + "containerId" + ), + }, + }) + + // Create metadata if provided + if (metadata) { + await tx.credentialMetadata.create({ + data: { + credentialId: credential.id, + recoveryEmail: metadata.recoveryEmail, + phoneNumber: metadata.phoneNumber, + otherInfo: metadata.otherInfo || [], + has2FA: metadata.has2FA || false, + }, + }) + } + + return credential + }) + + return { + success: true, + credential: CredentialEntity.getSimpleRo(result), + } + } catch (error) { + console.error("Error creating credential with metadata:", error) + + // If it's an ORPCError, re-throw it to maintain consistent error handling + if (error instanceof ORPCError) { + throw error + } + + return { + success: false, + error: + error instanceof Error ? error.message : "Unknown error occurred", + } + } + } + ) + +// Export the credential router +export const credentialRouter = { + get: getCredential, + list: listCredentials, + create: createCredential, + createWithMetadata: createCredentialWithMetadata, + update: updateCredential, + delete: deleteCredential, +} diff --git a/orpc/routers/index.ts b/orpc/routers/index.ts new file mode 100644 index 0000000..63525bd --- /dev/null +++ b/orpc/routers/index.ts @@ -0,0 +1,19 @@ +import { cardRouter } from "./card" +import { containerRouter } from "./container" +import { credentialRouter } from "./credential" +import { platformRouter } from "./platform" +import { secretRouter } from "./secret" +import { tagRouter } from "./tag" +import { userRouter } from "./user" + +export const appRouter = { + cards: cardRouter, + credentials: credentialRouter, + secrets: secretRouter, + containers: containerRouter, + platforms: platformRouter, + tags: tagRouter, + users: userRouter, +} + +export type AppRouter = typeof appRouter diff --git a/orpc/routers/platform.ts b/orpc/routers/platform.ts new file mode 100644 index 0000000..514107e --- /dev/null +++ b/orpc/routers/platform.ts @@ -0,0 +1,72 @@ +import { authMiddleware } from "@/middleware/auth" +import { database } from "@/prisma/client" +import { + createPlatformInputSchema, + deletePlatformInputSchema, + getPlatformInputSchema, + listPlatformsInputSchema, + listPlatformsOutputSchema, + platformOutputSchema, + updatePlatformInputSchema, + type ListPlatformsOutput, + type PlatformOutput, +} from "@/schemas/utils/dto" +import { ORPCError, os } from "@orpc/server" + +import type { ORPCContext } from "../types" + +const baseProcedure = os.$context() +const authProcedure = baseProcedure.use(({ context, next }) => + authMiddleware({ context, next }) +) + +// List platforms with pagination +export const listPlatforms = authProcedure + .input(listPlatformsInputSchema) + .output(listPlatformsOutputSchema) + .handler(async ({ input, context }): Promise => { + const { page, limit, search } = input + const skip = (page - 1) * limit + + const where = { + OR: [ + { userId: context.user.id }, + { userId: null }, // Global platforms + ], + ...(search && { + name: { contains: search, mode: "insensitive" as const }, + }), + } + + const [platforms, total] = await Promise.all([ + database.platform.findMany({ + where, + skip, + take: limit, + orderBy: { name: "asc" }, + }), + database.platform.count({ where }), + ]) + + return { + platforms: platforms.map((platform) => ({ + id: platform.id, + name: platform.name, + status: platform.status, + logo: platform.logo, + loginUrl: platform.loginUrl, + updatedAt: platform.updatedAt, + createdAt: platform.createdAt, + userId: platform.userId, + })), + total, + hasMore: skip + platforms.length < total, + page, + limit, + } + }) + +// Export the platform router +export const platformRouter = { + list: listPlatforms, +} diff --git a/orpc/routers/secret.ts b/orpc/routers/secret.ts new file mode 100644 index 0000000..3ed1b02 --- /dev/null +++ b/orpc/routers/secret.ts @@ -0,0 +1,209 @@ +import { SecretEntity } from "@/entities/secrets" +import { authMiddleware } from "@/middleware/auth" +import { database } from "@/prisma/client" +import { + createSecretInputSchema, + deleteSecretInputSchema, + getSecretInputSchema, + listSecretsInputSchema, + listSecretsOutputSchema, + secretOutputSchema, + updateSecretInputSchema, + type ListSecretsOutput, + type SecretOutput, +} from "@/schemas/secrets/dto" +import { ORPCError, os } from "@orpc/server" +import type { Prisma } from "@prisma/client" + +import { createEncryptedData } from "@/lib/utils/encryption-helpers" + +import type { ORPCContext } from "../types" + +const baseProcedure = os.$context() +const authProcedure = baseProcedure.use(({ context, next }) => + authMiddleware({ context, next }) +) + +// Get secret by ID +export const getSecret = authProcedure + .input(getSecretInputSchema) + .output(secretOutputSchema) + .handler(async ({ input, context }): Promise => { + const secret = await database.secret.findFirst({ + where: { + id: input.id, + userId: context.user.id, + }, + }) + + if (!secret) { + throw new ORPCError("NOT_FOUND") + } + + // Update last viewed timestamp + await database.secret.update({ + where: { id: input.id }, + data: { lastViewed: new Date() }, + }) + + return SecretEntity.getSimpleRo(secret) + }) + +// List secrets with pagination +export const listSecrets = authProcedure + .input(listSecretsInputSchema) + .output(listSecretsOutputSchema) + .handler(async ({ input, context }): Promise => { + const { page, limit, search, containerId } = input + const skip = (page - 1) * limit + + const where = { + userId: context.user.id, + ...(containerId && { containerId }), + ...(search && { + OR: [ + { name: { contains: search, mode: "insensitive" as const } }, + { note: { contains: search, mode: "insensitive" as const } }, + ], + }), + } + + const [secrets, total] = await Promise.all([ + database.secret.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: "desc" }, + }), + database.secret.count({ where }), + ]) + + return { + secrets: secrets.map((secret) => SecretEntity.getSimpleRo(secret)), + total, + hasMore: skip + secrets.length < total, + page, + limit, + } + }) + +// Create secret +export const createSecret = authProcedure + .input(createSecretInputSchema) + .output(secretOutputSchema) + .handler(async ({ input, context }): Promise => { + // Create encrypted data for value + const valueEncryptionResult = await createEncryptedData({ + encryptedValue: input.valueEncryption.encryptedValue, + encryptionKey: input.valueEncryption.encryptionKey, + iv: input.valueEncryption.iv, + }) + + if ( + !valueEncryptionResult.success || + !valueEncryptionResult.encryptedData + ) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + const secret = await database.secret.create({ + data: { + name: input.name, + note: input.note, + valueEncryptionId: valueEncryptionResult.encryptedData.id, + containerId: input.containerId, + userId: context.user.id, + }, + }) + + return SecretEntity.getSimpleRo(secret) + }) + +// Update secret +export const updateSecret = authProcedure + .input(updateSecretInputSchema) + .output(secretOutputSchema) + .handler(async ({ input, context }): Promise => { + const { id, ...updateData } = input + + // Verify secret ownership + const existingSecret = await database.secret.findFirst({ + where: { + id, + userId: context.user.id, + }, + }) + + if (!existingSecret) { + throw new ORPCError("NOT_FOUND") + } + + // Process the update data + const updatePayload: Prisma.SecretUpdateInput = {} + + if (updateData.name !== undefined) updatePayload.name = updateData.name + if (updateData.note !== undefined) updatePayload.note = updateData.note + if (updateData.containerId) { + updatePayload.container = { connect: { id: updateData.containerId } } + } + + // Handle value encryption updates if provided + if (updateData.valueEncryption) { + const valueEncryptionResult = await createEncryptedData({ + encryptedValue: updateData.valueEncryption.encryptedValue, + encryptionKey: updateData.valueEncryption.encryptionKey, + iv: updateData.valueEncryption.iv, + }) + + if ( + !valueEncryptionResult.success || + !valueEncryptionResult.encryptedData + ) { + throw new ORPCError("INTERNAL_SERVER_ERROR") + } + + updatePayload.valueEncryption = { + connect: { id: valueEncryptionResult.encryptedData.id }, + } + } + + const updatedSecret = await database.secret.update({ + where: { id }, + data: updatePayload, + }) + + return SecretEntity.getSimpleRo(updatedSecret) + }) + +// Delete secret +export const deleteSecret = authProcedure + .input(deleteSecretInputSchema) + .output(secretOutputSchema) + .handler(async ({ input, context }): Promise => { + // Verify secret ownership + const existingSecret = await database.secret.findFirst({ + where: { + id: input.id, + userId: context.user.id, + }, + }) + + if (!existingSecret) { + throw new ORPCError("NOT_FOUND") + } + + const deletedSecret = await database.secret.delete({ + where: { id: input.id }, + }) + + return SecretEntity.getSimpleRo(deletedSecret) + }) + +// Export the secret router +export const secretRouter = { + get: getSecret, + list: listSecrets, + create: createSecret, + update: updateSecret, + delete: deleteSecret, +} diff --git a/orpc/routers/tag.ts b/orpc/routers/tag.ts new file mode 100644 index 0000000..f7981a8 --- /dev/null +++ b/orpc/routers/tag.ts @@ -0,0 +1,67 @@ +import { authMiddleware } from "@/middleware/auth" +import { database } from "@/prisma/client" +import { + createTagInputSchema, + deleteTagInputSchema, + getTagInputSchema, + listTagsInputSchema, + listTagsOutputSchema, + tagOutputSchema, + updateTagInputSchema, + type ListTagsOutput, + type TagOutput, +} from "@/schemas/utils/dto" +import { ORPCError, os } from "@orpc/server" + +import type { ORPCContext } from "../types" + +const baseProcedure = os.$context() +const authProcedure = baseProcedure.use(({ context, next }) => + authMiddleware({ context, next }) +) + +// List tags with pagination +export const listTags = authProcedure + .input(listTagsInputSchema) + .output(listTagsOutputSchema) + .handler(async ({ input, context }): Promise => { + const { page, limit, search, containerId } = input + const skip = (page - 1) * limit + + const where = { + userId: context.user.id, + ...(containerId && { containerId }), + ...(search && { + name: { contains: search, mode: "insensitive" as const }, + }), + } + + const [tags, total] = await Promise.all([ + database.tag.findMany({ + where, + skip, + take: limit, + orderBy: { name: "asc" }, + }), + database.tag.count({ where }), + ]) + + return { + tags: tags.map((tag) => ({ + id: tag.id, + name: tag.name, + color: tag.color, + userId: tag.userId, + containerId: tag.containerId, + })), + total, + hasMore: skip + tags.length < total, + page, + limit, + } + }) + +// Export the tag router +export const tagRouter = { + list: listTags, +} diff --git a/orpc/routers/user.ts b/orpc/routers/user.ts new file mode 100644 index 0000000..cfde1e7 --- /dev/null +++ b/orpc/routers/user.ts @@ -0,0 +1,146 @@ +import { database } from "@/prisma/client" +import { + getEncryptedDataCountOutputSchema, + getUserCountOutputSchema, + type GetEncryptedDataCountOutput, + type GetUserCountOutput, +} from "@/schemas/user/statistics" +import { + getWaitlistCountOutputSchema, + joinWaitlistInputSchema, + joinWaitlistOutputSchema, + type GetWaitlistCountOutput, + type JoinWaitlistInput, + type JoinWaitlistOutput, +} from "@/schemas/user/waitlist" +import { ORPCError, os } from "@orpc/server" +import { Prisma } from "@prisma/client" +import { z } from "zod" + +import type { ORPCContext } from "../types" + +const baseProcedure = os.$context() +const publicProcedure = baseProcedure.use(({ context, next }) => { + return next({ context }) +}) + +// Join waitlist +export const joinWaitlist = publicProcedure + .input(joinWaitlistInputSchema) + .output(joinWaitlistOutputSchema) + .handler(async ({ input }): Promise => { + try { + // Check if email already exists in waitlist + const existingWaitlistEntry = await database.waitlist.findUnique({ + where: { email: input.email }, + }) + + if (existingWaitlistEntry) { + return { + success: false, + error: "Email is already on the waitlist", + } + } + + // Add to waitlist + await database.waitlist.create({ + data: { + email: input.email, + }, + }) + + return { success: true } + } catch (error) { + // Re-throw ORPC errors to let ORPC handle them + if (error instanceof ORPCError) { + throw error + } + + // Handle Prisma-specific errors + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error("Database constraint error joining waitlist:", { + code: error.code, + message: error.message, + meta: error.meta, + }) + + // Handle unique constraint violations + if (error.code === "P2002") { + return { + success: false, + error: "Email is already on the waitlist", + } + } + + // Handle other known Prisma errors + return { + success: false, + error: "Database constraint violation occurred", + } + } + + // Handle Prisma client errors (connection issues, etc.) + if (error instanceof Prisma.PrismaClientUnknownRequestError) { + console.error("Unknown Prisma error joining waitlist:", { + message: error.message, + }) + return { + success: false, + error: "Database connection issue occurred", + } + } + + // Handle Prisma validation errors + if (error instanceof Prisma.PrismaClientValidationError) { + console.error("Prisma validation error joining waitlist:", { + message: error.message, + }) + return { + success: false, + error: "Invalid data provided", + } + } + + // Handle unexpected errors + console.error("Unexpected error joining waitlist:", error) + return { + success: false, + error: "An unexpected error occurred. Please try again later.", + } + } + }) + +// Get waitlist count +export const getWaitlistCount = publicProcedure + .input(z.object({})) + .output(getWaitlistCountOutputSchema) + .handler(async (): Promise => { + const total = await database.waitlist.count() + return { total } + }) + +// Get user count +export const getUserCount = publicProcedure + .input(z.object({})) + .output(getUserCountOutputSchema) + .handler(async (): Promise => { + const total = await database.user.count() + return { total } + }) + +// Get encrypted data count +export const getEncryptedDataCount = publicProcedure + .input(z.object({})) + .output(getEncryptedDataCountOutputSchema) + .handler(async (): Promise => { + const count = await database.encryptedData.count() + return { count } + }) + +// Export the user router +export const userRouter = { + joinWaitlist, + getWaitlistCount, + getUserCount, + getEncryptedDataCount, +} diff --git a/orpc/types.ts b/orpc/types.ts new file mode 100644 index 0000000..5ba671f --- /dev/null +++ b/orpc/types.ts @@ -0,0 +1,13 @@ +import type { Session, User } from "better-auth/types" + +export interface ORPCContext { + session: Session | null + user: User | null +} + +export interface AuthenticatedContext extends ORPCContext { + session: Session + user: User +} + +export type PublicContext = ORPCContext diff --git a/package.json b/package.json index 43c856e..7ad5101 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,11 @@ "dependencies": { "@hookform/resolvers": "^5.0.1", "@neondatabase/serverless": "^0.10.4", + "@orpc/client": "^1.5.2", + "@orpc/next": "^0.27.0", + "@orpc/react-query": "^1.5.2", + "@orpc/server": "^1.5.2", + "@orpc/tanstack-query": "^1.5.2", "@prisma/adapter-neon": "6.9.0", "@prisma/client": "^6.9.0", "@radix-ui/react-accordion": "^1.2.10", @@ -55,7 +60,8 @@ "@radix-ui/react-toggle-group": "^1.1.9", "@radix-ui/react-tooltip": "^1.2.6", "@t3-oss/env-nextjs": "^0.13.4", - "@tanstack/react-query": "^5.75.7", + "@tanstack/react-query": "^5.80.7", + "@tanstack/react-query-devtools": "^4.39.2", "@vercel/analytics": "^1.5.0", "better-auth": "^1.2.7", "class-variance-authority": "^0.7.1", @@ -79,6 +85,7 @@ "recharts": "^2.15.3", "server-only": "^0.0.1", "sonner": "^2.0.3", + "superjson": "^2.2.2", "tailwind-merge": "^3.2.0", "vaul": "^1.1.2", "ws": "^8.18.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef30f58..a219467 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,21 @@ importers: "@neondatabase/serverless": specifier: ^0.10.4 version: 0.10.4 + "@orpc/client": + specifier: ^1.5.2 + version: 1.5.2 + "@orpc/next": + specifier: ^0.27.0 + version: 0.27.0(@orpc/server@1.5.2(ws@8.18.2(bufferutil@4.0.9)))(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + "@orpc/react-query": + specifier: ^1.5.2 + version: 1.5.2(@orpc/client@1.5.2)(@tanstack/query-core@5.80.7)(@tanstack/react-query@5.80.7(react@19.1.0))(react@19.1.0) + "@orpc/server": + specifier: ^1.5.2 + version: 1.5.2(ws@8.18.2(bufferutil@4.0.9)) + "@orpc/tanstack-query": + specifier: ^1.5.2 + version: 1.5.2(@orpc/client@1.5.2)(@tanstack/query-core@5.80.7) "@prisma/adapter-neon": specifier: 6.9.0 version: 6.9.0(@neondatabase/serverless@0.10.4) @@ -101,8 +116,11 @@ importers: specifier: ^0.13.4 version: 0.13.4(arktype@2.1.20)(typescript@5.8.3)(zod@3.24.4) "@tanstack/react-query": - specifier: ^5.75.7 - version: 5.75.7(react@19.1.0) + specifier: ^5.80.7 + version: 5.80.7(react@19.1.0) + "@tanstack/react-query-devtools": + specifier: ^4.39.2 + version: 4.39.2(@tanstack/react-query@5.80.7(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) "@vercel/analytics": specifier: ^1.5.0 version: 1.5.0(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) @@ -172,6 +190,9 @@ importers: sonner: specifier: ^2.0.3 version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + superjson: + specifier: ^2.2.2 + version: 2.2.2 tailwind-merge: specifier: ^3.2.0 version: 3.2.0 @@ -917,6 +938,124 @@ packages: } engines: { node: ">=12.4.0" } + "@orpc/client@1.5.2": + resolution: + { + integrity: sha512-O8zr+YA9oMNjr9n0XbwDkiamtE/OIqz8ynoohFVZ5bki17hYvkJFbVtfgS3y+q43ORbHpHDSEt0NIOKWalz6dA==, + } + + "@orpc/contract@0.27.0": + resolution: + { + integrity: sha512-0DHroTEQUNC6O/yvE93+hzjgRZxF+Z+NCidJ6A3CAz1H8oiftE+rm7DsOiKjm1bcUuiUD0zPEKXsG3qYaclZTQ==, + } + + "@orpc/contract@1.5.2": + resolution: + { + integrity: sha512-mXStakJPqY42iEltah5bRDAqnFEZor84SFzhTBac8pdSVrrEVgs6AkiYkwubkLV1NNwN8rqIFc3QDchLgJxEJQ==, + } + + "@orpc/next@0.27.0": + resolution: + { + integrity: sha512-b4+u2uNSmFxEDTw0otGbUs/QVoGrX3vp3HZVCLLqPwdIEVQxFUi5ONcqlYA8zg7RWb8tYr6GdEnfp2C+jSWviA==, + } + peerDependencies: + "@orpc/server": 0.27.0 + next: ">=15.1.0" + react: ">=18.3.0" + + "@orpc/openapi@0.27.0": + resolution: + { + integrity: sha512-hj242fVuczVKlcJFxHNahgp76rRqQvnbn1ejVOQCstCp0MB4B9nPWk17gHoR+0e1shAXHi8qA90sZizYrmeoZA==, + } + + "@orpc/react-query@1.5.2": + resolution: + { + integrity: sha512-Bm1l6XJWvoS4X/X/NwUVLS2rjQbRbSG6zcpHPlQShid04IDsyaonAGWdzI3L+ehh/qzDwM/7ffk4tWc2wSac8Q==, + } + peerDependencies: + "@orpc/client": 1.5.2 + "@tanstack/react-query": ">=5.80.2" + react: ">=18.3.0" + + "@orpc/server@0.27.0": + resolution: + { + integrity: sha512-mj3WSsEplfknJ8L3dbdkcfXM2550XciDyC7Kt+8ttKS/Z5h9QGnyUZM5J5CDXrYwXOPfdNjqzB4ncZPR8BXJzw==, + } + peerDependencies: + hono: ">=4.6.0" + next: ">=14.0.0" + + "@orpc/server@1.5.2": + resolution: + { + integrity: sha512-KegQlqfMXBoHMq4PKkOLsjHRJxG8QeFFOJtT7cs8b4w2+HRzelJb70Han4wny8ih023IWTj68SkyMc/iAUkyaw==, + } + peerDependencies: + crossws: ">=0.3.4" + ws: ">=8.18.1" + peerDependenciesMeta: + crossws: + optional: true + ws: + optional: true + + "@orpc/shared@0.27.0": + resolution: + { + integrity: sha512-w64lrtQYJsKhUDj2mwyeyCUmdKWIw/gA4N3m7SEis4J0ThtX5WTLWSTUYXje85U4yEr8nGvXew3k6q5qypM/Pg==, + } + + "@orpc/shared@1.5.2": + resolution: + { + integrity: sha512-cOfXv2EJ3OtcDQF1I0/0JS264zcexanm6bPOA7BHFu0sCivcUhMMoh5egKO4P2GWfDUbXSdH9Z5MDHBiN6IIZw==, + } + + "@orpc/standard-server-aws-lambda@1.5.2": + resolution: + { + integrity: sha512-1Lzb4BxuaSHgeoBHQp5eaIKdgWEz9tLifUQBO6dpcuhVcKvEZ2YYFyKCFJNRoypExf9j6qNiLV+aienmpVRviA==, + } + + "@orpc/standard-server-fetch@1.5.2": + resolution: + { + integrity: sha512-kpkex8l2MhzyP7hF+jYrH4UCLtSOv+bXAtESJklZrF2y3qRIaCrkEilhXxbMyuk/mkjmamra0JEpita+be4tGA==, + } + + "@orpc/standard-server-node@1.5.2": + resolution: + { + integrity: sha512-i8mNySlwGzglRW3f4g78XPuwK6ahfliL/6YKzWim0LRfW5vyWCz8yrqfkfxQorncEr8yyFAqnWKhNf3o4iaknw==, + } + + "@orpc/standard-server-peer@1.5.2": + resolution: + { + integrity: sha512-RPL6VcFuakdaBZIYGWdu7QlaeFK5RT52dRXOkfd1pCOWB7HTa2wMAWFgjwV2deX25JuBUA8wDP2HNdOi4qWamQ==, + } + + "@orpc/standard-server@1.5.2": + resolution: + { + integrity: sha512-j7xFPzr4ijmNTrN5ViIW2VAi5BRPcREXKKz2jU8C+b/47FqS1iqe6/MmS6PSPSBDuZA5s/D21VRMsrQz0Q4vzg==, + } + + "@orpc/tanstack-query@1.5.2": + resolution: + { + integrity: sha512-6yPTEz4+V8+djLhE6NO30gKlt3Ex0n3haH54Km0O6k0Y5L4sPYPyhqap47o5Zm7X3C3WYUwV0EQ9MWZfwCP8qw==, + } + peerDependencies: + "@orpc/client": 1.5.2 + "@tanstack/query-core": ">=5.80.2" + "@peculiar/asn1-android@2.3.16": resolution: { @@ -1818,6 +1957,18 @@ packages: } engines: { node: ">=20.0.0" } + "@standard-schema/spec@1.0.0": + resolution: + { + integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==, + } + + "@standard-schema/spec@1.0.0-beta.4": + resolution: + { + integrity: sha512-d3IxtzLo7P1oZ8s8YNvxzBUXRXojSut8pbPrTYtzsc5sn4+53jVqbk66pQerSZbZSJZQux6LkclB/+8IDordHg==, + } + "@standard-schema/utils@0.3.0": resolution: { @@ -2004,16 +2155,33 @@ packages: integrity: sha512-ELq+gDMBuRXPJlpE3PEen+1MhnHAQQrh2zF0dI1NXOlEWfr2qWf2CQdr5jl9yANv8RErQaQ2l6nIFO9OSCVq/g==, } - "@tanstack/query-core@5.75.7": + "@tanstack/match-sorter-utils@8.19.4": resolution: { - integrity: sha512-4BHu0qnxUHOSnTn3ow9fIoBKTelh0GY08yn1IO9cxjBTsGvnxz1ut42CHZqUE3Vl/8FAjcHsj8RNJMoXvjgHEA==, + integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==, } + engines: { node: ">=12" } + + "@tanstack/query-core@5.80.7": + resolution: + { + integrity: sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==, + } + + "@tanstack/react-query-devtools@4.39.2": + resolution: + { + integrity: sha512-xAFdXbH20Tzfge7qLFLndof90a6DG8WbRPhDVt9CQZPy5kQKwemnBknadR5tm0cZql8u+nyv+j/qEt9hdpFSog==, + } + peerDependencies: + "@tanstack/react-query": ^4.39.2 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - "@tanstack/react-query@5.75.7": + "@tanstack/react-query@5.80.7": resolution: { - integrity: sha512-JYcH1g5pNjKXNQcvvnCU/PueaYg05uKBDHlWIyApspv7r5C0BM12n6ysa2QF2T+1tlPnNXOob8vr8o96Nx0GxQ==, + integrity: sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==, } peerDependencies: react: ^18 || ^19 @@ -2055,6 +2223,12 @@ packages: } deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + "@types/content-disposition@0.5.9": + resolution: + { + integrity: sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==, + } + "@types/d3-array@3.2.1": resolution: { @@ -2765,6 +2939,13 @@ packages: integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, } + content-disposition@0.5.4: + resolution: + { + integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==, + } + engines: { node: ">= 0.6" } + content-disposition@1.0.0: resolution: { @@ -2793,6 +2974,13 @@ packages: } engines: { node: ">= 0.6" } + copy-anything@3.0.5: + resolution: + { + integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==, + } + engines: { node: ">=12.13" } + cors@2.8.5: resolution: { @@ -3161,6 +3349,13 @@ packages: } engines: { node: ">=10" } + escape-string-regexp@5.0.0: + resolution: + { + integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==, + } + engines: { node: ">=12" } + eslint-config-next@15.3.2: resolution: { @@ -3397,6 +3592,12 @@ packages: } engines: { node: ">= 18" } + fast-content-type-parse@2.0.1: + resolution: + { + integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==, + } + fast-deep-equal@3.1.3: resolution: { @@ -3701,6 +3902,13 @@ packages: } engines: { node: ">= 0.4" } + hono@4.7.11: + resolution: + { + integrity: sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ==, + } + engines: { node: ">=16.9.0" } + http-errors@2.0.0: resolution: { @@ -3958,6 +4166,20 @@ packages: } engines: { node: ">= 0.4" } + is-what@4.1.16: + resolution: + { + integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==, + } + engines: { node: ">=12.13" } + + is-what@5.2.1: + resolution: + { + integrity: sha512-FLNNgur29o+0/G6RcG3B6KRDCT6SvMfb7MlfjdydTZWPgBLfiemceChDhY0DHu50O35BDNbNp4rJLQXMt4fG0g==, + } + engines: { node: ">=18" } + isarray@2.0.5: resolution: { @@ -4023,6 +4245,12 @@ packages: integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, } + json-schema-typed@8.0.1: + resolution: + { + integrity: sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==, + } + json-stable-stringify-without-jsonify@1.0.1: resolution: { @@ -4495,6 +4723,18 @@ packages: integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, } + openapi-types@12.1.3: + resolution: + { + integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==, + } + + openapi3-ts@4.4.0: + resolution: + { + integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==, + } + optionator@0.9.4: resolution: { @@ -4822,6 +5062,13 @@ packages: integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, } + radash@12.1.0: + resolution: + { + integrity: sha512-b0Zcf09AhqKS83btmUeYBS8tFK7XL2e3RvLmZcm0sTdF1/UUlHSsjXdCcWNxe7yfmAlPve5ym0DmKGtTzP6kVQ==, + } + engines: { node: ">=14.18.0" } + range-parser@1.2.1: resolution: { @@ -4995,6 +5242,12 @@ packages: } engines: { node: ">= 0.4" } + remove-accents@0.5.0: + resolution: + { + integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==, + } + resolve-from@4.0.0: resolution: { @@ -5340,6 +5593,20 @@ packages: integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==, } + superjson@1.13.3: + resolution: + { + integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==, + } + engines: { node: ">=10" } + + superjson@2.2.2: + resolution: + { + integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==, + } + engines: { node: ">=16" } + supports-color@7.2.0: resolution: { @@ -5471,6 +5738,13 @@ packages: } engines: { node: ">= 0.8.0" } + type-fest@4.41.0: + resolution: + { + integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==, + } + engines: { node: ">=16" } + type-is@2.0.1: resolution: { @@ -5650,6 +5924,12 @@ packages: engines: { node: ">= 8" } hasBin: true + wildcard-match@5.1.4: + resolution: + { + integrity: sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==, + } + word-wrap@1.2.5: resolution: { @@ -5685,6 +5965,14 @@ packages: } engines: { node: ">=18" } + yaml@2.8.0: + resolution: + { + integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==, + } + engines: { node: ">= 14.6" } + hasBin: true + yn@3.1.1: resolution: { @@ -6081,6 +6369,126 @@ snapshots: "@nolyfill/is-core-module@1.0.39": {} + "@orpc/client@1.5.2": + dependencies: + "@orpc/shared": 1.5.2 + "@orpc/standard-server": 1.5.2 + "@orpc/standard-server-fetch": 1.5.2 + "@orpc/standard-server-peer": 1.5.2 + + "@orpc/contract@0.27.0": + dependencies: + "@orpc/shared": 0.27.0 + "@standard-schema/spec": 1.0.0-beta.4 + + "@orpc/contract@1.5.2": + dependencies: + "@orpc/client": 1.5.2 + "@orpc/shared": 1.5.2 + "@standard-schema/spec": 1.0.0 + openapi-types: 12.1.3 + + "@orpc/next@0.27.0(@orpc/server@1.5.2(ws@8.18.2(bufferutil@4.0.9)))(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)": + dependencies: + "@orpc/contract": 0.27.0 + "@orpc/openapi": 0.27.0(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + "@orpc/server": 1.5.2(ws@8.18.2(bufferutil@4.0.9)) + "@orpc/shared": 0.27.0 + next: 15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + + "@orpc/openapi@0.27.0(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))": + dependencies: + "@orpc/contract": 0.27.0 + "@orpc/server": 0.27.0(hono@4.7.11)(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + "@orpc/shared": 0.27.0 + "@standard-schema/spec": 1.0.0-beta.4 + "@types/content-disposition": 0.5.9 + content-disposition: 0.5.4 + escape-string-regexp: 5.0.0 + fast-content-type-parse: 2.0.1 + hono: 4.7.11 + json-schema-typed: 8.0.1 + openapi3-ts: 4.4.0 + wildcard-match: 5.1.4 + transitivePeerDependencies: + - next + + "@orpc/react-query@1.5.2(@orpc/client@1.5.2)(@tanstack/query-core@5.80.7)(@tanstack/react-query@5.80.7(react@19.1.0))(react@19.1.0)": + dependencies: + "@orpc/client": 1.5.2 + "@orpc/shared": 1.5.2 + "@orpc/tanstack-query": 1.5.2(@orpc/client@1.5.2)(@tanstack/query-core@5.80.7) + "@tanstack/react-query": 5.80.7(react@19.1.0) + react: 19.1.0 + transitivePeerDependencies: + - "@tanstack/query-core" + + "@orpc/server@0.27.0(hono@4.7.11)(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))": + dependencies: + "@orpc/contract": 0.27.0 + "@orpc/shared": 0.27.0 + hono: 4.7.11 + next: 15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + + "@orpc/server@1.5.2(ws@8.18.2(bufferutil@4.0.9))": + dependencies: + "@orpc/client": 1.5.2 + "@orpc/contract": 1.5.2 + "@orpc/shared": 1.5.2 + "@orpc/standard-server": 1.5.2 + "@orpc/standard-server-aws-lambda": 1.5.2 + "@orpc/standard-server-fetch": 1.5.2 + "@orpc/standard-server-node": 1.5.2 + "@orpc/standard-server-peer": 1.5.2 + optionalDependencies: + ws: 8.18.2(bufferutil@4.0.9) + + "@orpc/shared@0.27.0": + dependencies: + "@standard-schema/spec": 1.0.0-beta.4 + is-what: 5.2.1 + radash: 12.1.0 + type-fest: 4.41.0 + + "@orpc/shared@1.5.2": + dependencies: + radash: 12.1.0 + type-fest: 4.41.0 + + "@orpc/standard-server-aws-lambda@1.5.2": + dependencies: + "@orpc/shared": 1.5.2 + "@orpc/standard-server": 1.5.2 + "@orpc/standard-server-fetch": 1.5.2 + "@orpc/standard-server-node": 1.5.2 + + "@orpc/standard-server-fetch@1.5.2": + dependencies: + "@orpc/shared": 1.5.2 + "@orpc/standard-server": 1.5.2 + + "@orpc/standard-server-node@1.5.2": + dependencies: + "@orpc/shared": 1.5.2 + "@orpc/standard-server": 1.5.2 + "@orpc/standard-server-fetch": 1.5.2 + + "@orpc/standard-server-peer@1.5.2": + dependencies: + "@orpc/shared": 1.5.2 + "@orpc/standard-server": 1.5.2 + + "@orpc/standard-server@1.5.2": + dependencies: + "@orpc/shared": 1.5.2 + + "@orpc/tanstack-query@1.5.2(@orpc/client@1.5.2)(@tanstack/query-core@5.80.7)": + dependencies: + "@orpc/client": 1.5.2 + "@orpc/shared": 1.5.2 + "@tanstack/query-core": 5.80.7 + "@peculiar/asn1-android@2.3.16": dependencies: "@peculiar/asn1-schema": 2.3.15 @@ -6826,6 +7234,10 @@ snapshots: "@peculiar/asn1-schema": 2.3.15 "@peculiar/asn1-x509": 2.3.15 + "@standard-schema/spec@1.0.0": {} + + "@standard-schema/spec@1.0.0-beta.4": {} + "@standard-schema/utils@0.3.0": {} "@swc/counter@0.1.3": {} @@ -6922,11 +7334,24 @@ snapshots: postcss: 8.5.3 tailwindcss: 4.1.6 - "@tanstack/query-core@5.75.7": {} + "@tanstack/match-sorter-utils@8.19.4": + dependencies: + remove-accents: 0.5.0 + + "@tanstack/query-core@5.80.7": {} + + "@tanstack/react-query-devtools@4.39.2(@tanstack/react-query@5.80.7(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)": + dependencies: + "@tanstack/match-sorter-utils": 8.19.4 + "@tanstack/react-query": 5.80.7(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + superjson: 1.13.3 + use-sync-external-store: 1.5.0(react@19.1.0) - "@tanstack/react-query@5.75.7(react@19.1.0)": + "@tanstack/react-query@5.80.7(react@19.1.0)": dependencies: - "@tanstack/query-core": 5.75.7 + "@tanstack/query-core": 5.80.7 react: 19.1.0 "@tsconfig/node10@1.0.11": {} @@ -6946,6 +7371,8 @@ snapshots: dependencies: bcryptjs: 3.0.2 + "@types/content-disposition@0.5.9": {} + "@types/d3-array@3.2.1": {} "@types/d3-color@3.1.3": {} @@ -7396,6 +7823,10 @@ snapshots: concat-map@0.0.1: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -7406,6 +7837,10 @@ snapshots: cookie@0.7.2: {} + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -7663,6 +8098,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-next@15.3.2(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3): dependencies: "@next/eslint-plugin-next": 15.3.2 @@ -7921,6 +8358,8 @@ snapshots: transitivePeerDependencies: - supports-color + fast-content-type-parse@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -8096,6 +8535,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.7.11: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -8248,6 +8689,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-what@4.1.16: {} + + is-what@5.2.1: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -8277,6 +8722,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-typed@8.0.1: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -8522,6 +8969,12 @@ snapshots: dependencies: wrappy: 1.0.2 + openapi-types@12.1.3: {} + + openapi3-ts@4.4.0: + dependencies: + yaml: 2.8.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -8663,6 +9116,8 @@ snapshots: queue-microtask@1.2.3: {} + radash@12.1.0: {} + range-parser@1.2.1: {} raw-body@3.0.0: @@ -8788,6 +9243,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remove-accents@0.5.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -9061,6 +9518,14 @@ snapshots: stylis@4.3.2: {} + superjson@1.13.3: + dependencies: + copy-anything: 3.0.5 + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -9139,6 +9604,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@4.41.0: {} + type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -9313,6 +9780,8 @@ snapshots: dependencies: isexe: 2.0.0 + wildcard-match@5.1.4: {} + word-wrap@1.2.5: {} wrappy@1.0.2: {} @@ -9323,6 +9792,8 @@ snapshots: yallist@5.0.0: {} + yaml@2.8.0: {} + yn@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/prettier.config.js b/prettier.config.js index 15413e9..fe4828c 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -24,8 +24,6 @@ module.exports = { "^@/components/(.*)$", "^@/components/ui/(.*)$", "", - "@/actions/(.*)", - "", "^[./]", ], tailwindFunctions: ["clsx", "cn", "twmerge", "cva"], diff --git a/prisma/schema/cards.prisma b/prisma/schema/cards.prisma index aae3925..a34ce4e 100644 --- a/prisma/schema/cards.prisma +++ b/prisma/schema/cards.prisma @@ -62,6 +62,7 @@ model Card { @@index([containerId]) @@index([cvvEncryptionId]) @@index([numberEncryptionId]) + @@map("card") } /** @@ -101,4 +102,5 @@ model CardMetadata { card Card @relation(fields: [cardId], references: [id], onDelete: Cascade) @@index([cardId]) + @@map("card_metadata") } diff --git a/prisma/schema/credential.prisma b/prisma/schema/credential.prisma index 0e28488..f70cbe1 100644 --- a/prisma/schema/credential.prisma +++ b/prisma/schema/credential.prisma @@ -51,6 +51,7 @@ model Credential { @@index([containerId]) @@index([platformId]) @@index([passwordEncryptionId]) + @@map("credential") } /** @@ -81,6 +82,7 @@ model CredentialHistory { @@index([credentialId]) @@index([userId]) @@index([passwordEncryptionId]) + @@map("credential_history") } /** @@ -107,4 +109,5 @@ model CredentialMetadata { credential Credential @relation(fields: [credentialId], references: [id]) @@index([credentialId]) + @@map("credential_metadata") } diff --git a/prisma/schema/encryption.prisma b/prisma/schema/encryption.prisma index 6cbf4f5..6d4d8c5 100644 --- a/prisma/schema/encryption.prisma +++ b/prisma/schema/encryption.prisma @@ -32,4 +32,5 @@ model EncryptedData { secretValue Secret[] @relation("SecretValue") @@index([createdAt]) + @@map("encrypted_data") } diff --git a/prisma/schema/migrations/20250616192028_usage_of_map/migration.sql b/prisma/schema/migrations/20250616192028_usage_of_map/migration.sql new file mode 100644 index 0000000..8e3a5c2 --- /dev/null +++ b/prisma/schema/migrations/20250616192028_usage_of_map/migration.sql @@ -0,0 +1,429 @@ +/* + Warnings: + + - You are about to drop the `Card` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `CardMetadata` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Container` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Credential` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `CredentialHistory` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `CredentialMetadata` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `EncryptedData` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Platform` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Secret` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `SecretMetadata` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Tag` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Card" DROP CONSTRAINT "Card_containerId_fkey"; + +-- DropForeignKey +ALTER TABLE "Card" DROP CONSTRAINT "Card_cvvEncryptionId_fkey"; + +-- DropForeignKey +ALTER TABLE "Card" DROP CONSTRAINT "Card_numberEncryptionId_fkey"; + +-- DropForeignKey +ALTER TABLE "Card" DROP CONSTRAINT "Card_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "CardMetadata" DROP CONSTRAINT "CardMetadata_cardId_fkey"; + +-- DropForeignKey +ALTER TABLE "Container" DROP CONSTRAINT "Container_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Credential" DROP CONSTRAINT "Credential_containerId_fkey"; + +-- DropForeignKey +ALTER TABLE "Credential" DROP CONSTRAINT "Credential_passwordEncryptionId_fkey"; + +-- DropForeignKey +ALTER TABLE "Credential" DROP CONSTRAINT "Credential_platformId_fkey"; + +-- DropForeignKey +ALTER TABLE "Credential" DROP CONSTRAINT "Credential_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "CredentialHistory" DROP CONSTRAINT "CredentialHistory_credentialId_fkey"; + +-- DropForeignKey +ALTER TABLE "CredentialHistory" DROP CONSTRAINT "CredentialHistory_passwordEncryptionId_fkey"; + +-- DropForeignKey +ALTER TABLE "CredentialHistory" DROP CONSTRAINT "CredentialHistory_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "CredentialMetadata" DROP CONSTRAINT "CredentialMetadata_credentialId_fkey"; + +-- DropForeignKey +ALTER TABLE "Platform" DROP CONSTRAINT "Platform_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Secret" DROP CONSTRAINT "Secret_containerId_fkey"; + +-- DropForeignKey +ALTER TABLE "Secret" DROP CONSTRAINT "Secret_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Secret" DROP CONSTRAINT "Secret_valueEncryptionId_fkey"; + +-- DropForeignKey +ALTER TABLE "SecretMetadata" DROP CONSTRAINT "SecretMetadata_secretId_fkey"; + +-- DropForeignKey +ALTER TABLE "Tag" DROP CONSTRAINT "Tag_containerId_fkey"; + +-- DropForeignKey +ALTER TABLE "Tag" DROP CONSTRAINT "Tag_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "_CardToTag" DROP CONSTRAINT "_CardToTag_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_CardToTag" DROP CONSTRAINT "_CardToTag_B_fkey"; + +-- DropForeignKey +ALTER TABLE "_CredentialToTag" DROP CONSTRAINT "_CredentialToTag_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_CredentialToTag" DROP CONSTRAINT "_CredentialToTag_B_fkey"; + +-- DropTable +DROP TABLE "Card"; + +-- DropTable +DROP TABLE "CardMetadata"; + +-- DropTable +DROP TABLE "Container"; + +-- DropTable +DROP TABLE "Credential"; + +-- DropTable +DROP TABLE "CredentialHistory"; + +-- DropTable +DROP TABLE "CredentialMetadata"; + +-- DropTable +DROP TABLE "EncryptedData"; + +-- DropTable +DROP TABLE "Platform"; + +-- DropTable +DROP TABLE "Secret"; + +-- DropTable +DROP TABLE "SecretMetadata"; + +-- DropTable +DROP TABLE "Tag"; + +-- CreateTable +CREATE TABLE "card" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "type" "CardType" NOT NULL, + "provider" "CardProvider" NOT NULL, + "status" "CardStatus" NOT NULL DEFAULT 'ACTIVE', + "cardholderName" TEXT NOT NULL, + "billingAddress" TEXT, + "cardholderEmail" TEXT, + "expiryDate" TIMESTAMP(3) NOT NULL, + "lastViewed" TIMESTAMP(3), + "updatedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "containerId" TEXT, + "numberEncryptionId" TEXT NOT NULL, + "cvvEncryptionId" TEXT NOT NULL, + + CONSTRAINT "card_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "card_metadata" ( + "id" TEXT NOT NULL, + "creditLimit" DECIMAL(65,30), + "availableCredit" DECIMAL(65,30), + "interestRate" DECIMAL(65,30), + "annualFee" DECIMAL(65,30), + "rewardsProgram" TEXT, + "contactlessEnabled" BOOLEAN NOT NULL DEFAULT false, + "onlinePaymentsEnabled" BOOLEAN NOT NULL DEFAULT true, + "internationalPaymentsEnabled" BOOLEAN NOT NULL DEFAULT true, + "pinSet" BOOLEAN NOT NULL DEFAULT false, + "otherInfo" JSONB[], + "cardId" TEXT NOT NULL, + + CONSTRAINT "card_metadata_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "credential" ( + "id" TEXT NOT NULL, + "identifier" TEXT NOT NULL, + "description" TEXT, + "status" "AccountStatus" NOT NULL DEFAULT 'ACTIVE', + "lastViewed" TIMESTAMP(3), + "updatedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "platformId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "containerId" TEXT, + "passwordEncryptionId" TEXT NOT NULL, + + CONSTRAINT "credential_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "credential_history" ( + "id" TEXT NOT NULL, + "changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "credentialId" TEXT NOT NULL, + "passwordEncryptionId" TEXT NOT NULL, + + CONSTRAINT "credential_history_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "credential_metadata" ( + "id" TEXT NOT NULL, + "recoveryEmail" TEXT, + "phoneNumber" TEXT, + "otherInfo" JSONB[], + "has2FA" BOOLEAN NOT NULL DEFAULT false, + "credentialId" TEXT NOT NULL, + + CONSTRAINT "credential_metadata_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "encrypted_data" ( + "id" TEXT NOT NULL, + "iv" TEXT NOT NULL, + "encryptedValue" TEXT NOT NULL, + "encryptionKey" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "encrypted_data_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "secret" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "note" TEXT, + "lastViewed" TIMESTAMP(3), + "updatedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "containerId" TEXT NOT NULL, + "valueEncryptionId" TEXT NOT NULL, + + CONSTRAINT "secret_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "secret_metadata" ( + "id" TEXT NOT NULL, + "type" "SecretType" NOT NULL, + "status" "SecretStatus" NOT NULL DEFAULT 'ACTIVE', + "expiresAt" TIMESTAMP(3), + "otherInfo" JSONB[], + "secretId" TEXT NOT NULL, + + CONSTRAINT "secret_metadata_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "platform" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "logo" TEXT, + "loginUrl" TEXT, + "status" "PlatformStatus" NOT NULL DEFAULT 'PENDING', + "updatedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT, + + CONSTRAINT "platform_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tag" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "color" TEXT, + "userId" TEXT, + "containerId" TEXT, + + CONSTRAINT "tag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "container" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "icon" TEXT NOT NULL, + "description" TEXT, + "type" "ContainerType" NOT NULL DEFAULT 'MIXED', + "updatedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + + CONSTRAINT "container_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "card_userId_idx" ON "card"("userId"); + +-- CreateIndex +CREATE INDEX "card_containerId_idx" ON "card"("containerId"); + +-- CreateIndex +CREATE INDEX "card_cvvEncryptionId_idx" ON "card"("cvvEncryptionId"); + +-- CreateIndex +CREATE INDEX "card_numberEncryptionId_idx" ON "card"("numberEncryptionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "card_metadata_cardId_key" ON "card_metadata"("cardId"); + +-- CreateIndex +CREATE INDEX "card_metadata_cardId_idx" ON "card_metadata"("cardId"); + +-- CreateIndex +CREATE INDEX "credential_userId_idx" ON "credential"("userId"); + +-- CreateIndex +CREATE INDEX "credential_containerId_idx" ON "credential"("containerId"); + +-- CreateIndex +CREATE INDEX "credential_platformId_idx" ON "credential"("platformId"); + +-- CreateIndex +CREATE INDEX "credential_passwordEncryptionId_idx" ON "credential"("passwordEncryptionId"); + +-- CreateIndex +CREATE INDEX "credential_history_credentialId_idx" ON "credential_history"("credentialId"); + +-- CreateIndex +CREATE INDEX "credential_history_userId_idx" ON "credential_history"("userId"); + +-- CreateIndex +CREATE INDEX "credential_history_passwordEncryptionId_idx" ON "credential_history"("passwordEncryptionId"); + +-- CreateIndex +CREATE INDEX "credential_metadata_credentialId_idx" ON "credential_metadata"("credentialId"); + +-- CreateIndex +CREATE INDEX "encrypted_data_createdAt_idx" ON "encrypted_data"("createdAt"); + +-- CreateIndex +CREATE INDEX "secret_userId_idx" ON "secret"("userId"); + +-- CreateIndex +CREATE INDEX "secret_containerId_idx" ON "secret"("containerId"); + +-- CreateIndex +CREATE INDEX "secret_valueEncryptionId_idx" ON "secret"("valueEncryptionId"); + +-- CreateIndex +CREATE INDEX "secret_metadata_secretId_idx" ON "secret_metadata"("secretId"); + +-- CreateIndex +CREATE INDEX "platform_userId_idx" ON "platform"("userId"); + +-- CreateIndex +CREATE INDEX "tag_userId_idx" ON "tag"("userId"); + +-- CreateIndex +CREATE INDEX "tag_containerId_idx" ON "tag"("containerId"); + +-- CreateIndex +CREATE INDEX "container_userId_idx" ON "container"("userId"); + +-- CreateIndex +CREATE INDEX "container_type_idx" ON "container"("type"); + +-- AddForeignKey +ALTER TABLE "card" ADD CONSTRAINT "card_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "card" ADD CONSTRAINT "card_containerId_fkey" FOREIGN KEY ("containerId") REFERENCES "container"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "card" ADD CONSTRAINT "card_numberEncryptionId_fkey" FOREIGN KEY ("numberEncryptionId") REFERENCES "encrypted_data"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "card" ADD CONSTRAINT "card_cvvEncryptionId_fkey" FOREIGN KEY ("cvvEncryptionId") REFERENCES "encrypted_data"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "card_metadata" ADD CONSTRAINT "card_metadata_cardId_fkey" FOREIGN KEY ("cardId") REFERENCES "card"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential" ADD CONSTRAINT "credential_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "platform"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential" ADD CONSTRAINT "credential_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential" ADD CONSTRAINT "credential_containerId_fkey" FOREIGN KEY ("containerId") REFERENCES "container"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential" ADD CONSTRAINT "credential_passwordEncryptionId_fkey" FOREIGN KEY ("passwordEncryptionId") REFERENCES "encrypted_data"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential_history" ADD CONSTRAINT "credential_history_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential_history" ADD CONSTRAINT "credential_history_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "credential"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential_history" ADD CONSTRAINT "credential_history_passwordEncryptionId_fkey" FOREIGN KEY ("passwordEncryptionId") REFERENCES "encrypted_data"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential_metadata" ADD CONSTRAINT "credential_metadata_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "credential"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "secret" ADD CONSTRAINT "secret_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "secret" ADD CONSTRAINT "secret_containerId_fkey" FOREIGN KEY ("containerId") REFERENCES "container"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "secret" ADD CONSTRAINT "secret_valueEncryptionId_fkey" FOREIGN KEY ("valueEncryptionId") REFERENCES "encrypted_data"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "secret_metadata" ADD CONSTRAINT "secret_metadata_secretId_fkey" FOREIGN KEY ("secretId") REFERENCES "secret"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "platform" ADD CONSTRAINT "platform_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tag" ADD CONSTRAINT "tag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tag" ADD CONSTRAINT "tag_containerId_fkey" FOREIGN KEY ("containerId") REFERENCES "container"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "container" ADD CONSTRAINT "container_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CardToTag" ADD CONSTRAINT "_CardToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "card"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CardToTag" ADD CONSTRAINT "_CardToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CredentialToTag" ADD CONSTRAINT "_CredentialToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "credential"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CredentialToTag" ADD CONSTRAINT "_CredentialToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema/secrets.prisma b/prisma/schema/secrets.prisma index f132069..713744b 100644 --- a/prisma/schema/secrets.prisma +++ b/prisma/schema/secrets.prisma @@ -38,6 +38,7 @@ model Secret { @@index([userId]) @@index([containerId]) @@index([valueEncryptionId]) + @@map("secret") } /** @@ -64,4 +65,5 @@ model SecretMetadata { secret Secret @relation(fields: [secretId], references: [id]) @@index([secretId]) + @@map("secret_metadata") } diff --git a/prisma/schema/utils.prisma b/prisma/schema/utils.prisma index 4e4057a..a16f6d2 100644 --- a/prisma/schema/utils.prisma +++ b/prisma/schema/utils.prisma @@ -36,6 +36,7 @@ model Platform { user User? @relation(fields: [userId], references: [id]) @@index([userId]) + @@map("platform") } /** @@ -64,6 +65,7 @@ model Tag { @@index([userId]) @@index([containerId]) + @@map("tag") } /** @@ -105,4 +107,5 @@ model Container { @@index([userId]) @@index([type]) + @@map("container") } diff --git a/schemas/card/card.ts b/schemas/card/card.ts index c150406..df67adf 100644 --- a/schemas/card/card.ts +++ b/schemas/card/card.ts @@ -2,7 +2,7 @@ import { encryptedDataDtoSchema } from "@/schemas/encryption/encryption" import { CardProvider, CardStatus, CardType } from "@prisma/client" import { z } from "zod" -import { CardExpiryDateUtils } from "@/lib/card-expiry-utils" +import { CardExpiryDateUtils } from "@/lib/utils/card-expiry-helpers" import { tagDtoSchema } from "../utils/tag" diff --git a/schemas/card/dto.ts b/schemas/card/dto.ts new file mode 100644 index 0000000..cd5976c --- /dev/null +++ b/schemas/card/dto.ts @@ -0,0 +1,44 @@ +import { z } from "zod" + +import { + cardDtoSchema, + cardSimpleRoSchema, + deleteCardDtoSchema, + getCardByIdDtoSchema, + updateCardDtoSchema, +} from "./card" + +// Input DTOs for oRPC procedures +export const createCardInputSchema = cardDtoSchema +export const getCardInputSchema = getCardByIdDtoSchema +export const updateCardInputSchema = updateCardDtoSchema +export const deleteCardInputSchema = deleteCardDtoSchema + +// List cards with pagination +export const listCardsInputSchema = z.object({ + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(10), + search: z.string().optional(), + containerId: z.string().optional(), +}) + +// Output DTOs for oRPC procedures +export const cardOutputSchema = cardSimpleRoSchema + +export const listCardsOutputSchema = z.object({ + cards: z.array(cardOutputSchema), + total: z.number().int(), + hasMore: z.boolean(), + page: z.number().int(), + limit: z.number().int(), +}) + +// Export types +export type CreateCardInput = z.infer +export type GetCardInput = z.infer +export type UpdateCardInput = z.infer +export type DeleteCardInput = z.infer +export type ListCardsInput = z.infer + +export type CardOutput = z.infer +export type ListCardsOutput = z.infer diff --git a/schemas/credential/credential-with-metadata.ts b/schemas/credential/credential-with-metadata.ts new file mode 100644 index 0000000..a3312ec --- /dev/null +++ b/schemas/credential/credential-with-metadata.ts @@ -0,0 +1,24 @@ +import { z } from "zod" + +import { credentialMetadataDtoSchema } from "./credential-metadata" +import { createCredentialInputSchema, credentialOutputSchema } from "./dto" + +// Schema for creating a credential with metadata +export const createCredentialWithMetadataInputSchema = z.object({ + credential: createCredentialInputSchema, + metadata: credentialMetadataDtoSchema.omit({ credentialId: true }).optional(), +}) + +export const createCredentialWithMetadataOutputSchema = z.object({ + success: z.boolean(), + credential: credentialOutputSchema.optional(), + error: z.string().optional(), + issues: z.array(z.string()).optional(), +}) + +export type CreateCredentialWithMetadataInput = z.infer< + typeof createCredentialWithMetadataInputSchema +> +export type CreateCredentialWithMetadataOutput = z.infer< + typeof createCredentialWithMetadataOutputSchema +> diff --git a/schemas/credential/dto.ts b/schemas/credential/dto.ts new file mode 100644 index 0000000..86e6b9b --- /dev/null +++ b/schemas/credential/dto.ts @@ -0,0 +1,45 @@ +import { z } from "zod" + +import { + credentialDtoSchema, + credentialSimpleRoSchema, + deleteCredentialDtoSchema, + getCredentialByIdDtoSchema, + updateCredentialDtoSchema, +} from "./credential" + +// Input DTOs for oRPC procedures +export const createCredentialInputSchema = credentialDtoSchema +export const getCredentialInputSchema = getCredentialByIdDtoSchema +export const updateCredentialInputSchema = updateCredentialDtoSchema +export const deleteCredentialInputSchema = deleteCredentialDtoSchema + +// List credentials with pagination +export const listCredentialsInputSchema = z.object({ + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(10), + search: z.string().optional(), + containerId: z.string().optional(), + platformId: z.string().optional(), +}) + +// Output DTOs for oRPC procedures +export const credentialOutputSchema = credentialSimpleRoSchema + +export const listCredentialsOutputSchema = z.object({ + credentials: z.array(credentialOutputSchema), + total: z.number().int(), + hasMore: z.boolean(), + page: z.number().int(), + limit: z.number().int(), +}) + +// Export types +export type CreateCredentialInput = z.infer +export type GetCredentialInput = z.infer +export type UpdateCredentialInput = z.infer +export type DeleteCredentialInput = z.infer +export type ListCredentialsInput = z.infer + +export type CredentialOutput = z.infer +export type ListCredentialsOutput = z.infer diff --git a/schemas/credential/index.ts b/schemas/credential/index.ts index 4c13f84..62e6431 100644 --- a/schemas/credential/index.ts +++ b/schemas/credential/index.ts @@ -1,3 +1,4 @@ export * from "./credential" +export * from "./credential-with-metadata" export * from "./credential-metadata" export * from "./credential-history" diff --git a/schemas/secrets/dto.ts b/schemas/secrets/dto.ts new file mode 100644 index 0000000..f4180ce --- /dev/null +++ b/schemas/secrets/dto.ts @@ -0,0 +1,44 @@ +import { z } from "zod" + +import { + deleteSecretDtoSchema, + getSecretByIdDtoSchema, + secretDtoSchema, + secretSimpleRoSchema, + updateSecretDtoSchema, +} from "./secret" + +// Input DTOs for oRPC procedures +export const createSecretInputSchema = secretDtoSchema +export const getSecretInputSchema = getSecretByIdDtoSchema +export const updateSecretInputSchema = updateSecretDtoSchema +export const deleteSecretInputSchema = deleteSecretDtoSchema + +// List secrets with pagination +export const listSecretsInputSchema = z.object({ + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(10), + search: z.string().optional(), + containerId: z.string().optional(), +}) + +// Output DTOs for oRPC procedures +export const secretOutputSchema = secretSimpleRoSchema + +export const listSecretsOutputSchema = z.object({ + secrets: z.array(secretOutputSchema), + total: z.number().int(), + hasMore: z.boolean(), + page: z.number().int(), + limit: z.number().int(), +}) + +// Export types +export type CreateSecretInput = z.infer +export type GetSecretInput = z.infer +export type UpdateSecretInput = z.infer +export type DeleteSecretInput = z.infer +export type ListSecretsInput = z.infer + +export type SecretOutput = z.infer +export type ListSecretsOutput = z.infer diff --git a/schemas/user/index.ts b/schemas/user/index.ts index 50c6bf2..cea004a 100644 --- a/schemas/user/index.ts +++ b/schemas/user/index.ts @@ -1 +1,3 @@ export * from "./user" +export * from "./waitlist" +export * from "./statistics" diff --git a/schemas/user/statistics.ts b/schemas/user/statistics.ts new file mode 100644 index 0000000..1bc1b4a --- /dev/null +++ b/schemas/user/statistics.ts @@ -0,0 +1,16 @@ +import { z } from "zod" + +// User statistics operations +export const getUserCountOutputSchema = z.object({ + total: z.number().int().min(0), +}) + +export const getEncryptedDataCountOutputSchema = z.object({ + count: z.number().int().min(0), +}) + +// Types +export type GetUserCountOutput = z.infer +export type GetEncryptedDataCountOutput = z.infer< + typeof getEncryptedDataCountOutputSchema +> diff --git a/schemas/user/waitlist.ts b/schemas/user/waitlist.ts new file mode 100644 index 0000000..cf1a0ac --- /dev/null +++ b/schemas/user/waitlist.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +// Waitlist operations +export const joinWaitlistInputSchema = z.object({ + email: z.string().email("Please enter a valid email address"), +}) + +export const joinWaitlistOutputSchema = z.object({ + success: z.boolean(), + error: z.string().optional(), +}) + +export const getWaitlistCountOutputSchema = z.object({ + total: z.number().int().min(0), +}) + +// Types +export type JoinWaitlistInput = z.infer +export type JoinWaitlistOutput = z.infer +export type GetWaitlistCountOutput = z.infer< + typeof getWaitlistCountOutputSchema +> diff --git a/schemas/utils/container-with-secrets.ts b/schemas/utils/container-with-secrets.ts new file mode 100644 index 0000000..08e62fe --- /dev/null +++ b/schemas/utils/container-with-secrets.ts @@ -0,0 +1,31 @@ +import { encryptedDataDtoSchema } from "@/schemas/encryption/encryption" +import { secretOutputSchema } from "@/schemas/secrets/dto" +import { z } from "zod" + +import { containerOutputSchema, createContainerInputSchema } from "./dto" + +// Schema for creating a container with secrets +export const createContainerWithSecretsInputSchema = z.object({ + container: createContainerInputSchema, + secrets: z.array( + z.object({ + name: z.string().min(1, "Secret name is required"), + note: z.string().optional(), + valueEncryption: encryptedDataDtoSchema, + }) + ), +}) + +export const createContainerWithSecretsOutputSchema = z.object({ + success: z.boolean(), + container: containerOutputSchema.optional(), + secrets: z.array(secretOutputSchema).optional(), + error: z.string().optional(), +}) + +export type CreateContainerWithSecretsInput = z.infer< + typeof createContainerWithSecretsInputSchema +> +export type CreateContainerWithSecretsOutput = z.infer< + typeof createContainerWithSecretsOutputSchema +> diff --git a/schemas/utils/dto.ts b/schemas/utils/dto.ts new file mode 100644 index 0000000..9068022 --- /dev/null +++ b/schemas/utils/dto.ts @@ -0,0 +1,118 @@ +import { z } from "zod" + +import { + containerDtoSchema, + containerSimpleRoSchema, + deleteContainerDtoSchema, + getContainerByIdDtoSchema, + updateContainerDtoSchema, +} from "./container" +import { + deletePlatformDtoSchema, + getPlatformByIdDtoSchema, + platformDtoSchema, + platformSimpleRoSchema, + updatePlatformDtoSchema, +} from "./platform" +import { + deleteTagDtoSchema, + getTagByIdDtoSchema, + tagDtoSchema, + tagSimpleRoSchema, + updateTagDtoSchema, +} from "./tag" + +// Container DTOs +export const createContainerInputSchema = containerDtoSchema +export const getContainerInputSchema = getContainerByIdDtoSchema +export const updateContainerInputSchema = updateContainerDtoSchema +export const deleteContainerInputSchema = deleteContainerDtoSchema + +export const listContainersInputSchema = z.object({ + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(10), + search: z.string().optional(), +}) + +export const containerOutputSchema = containerSimpleRoSchema + +export const listContainersOutputSchema = z.object({ + containers: z.array(containerOutputSchema), + total: z.number().int(), + hasMore: z.boolean(), + page: z.number().int(), + limit: z.number().int(), +}) + +// Platform DTOs +export const createPlatformInputSchema = platformDtoSchema +export const getPlatformInputSchema = getPlatformByIdDtoSchema +export const updatePlatformInputSchema = updatePlatformDtoSchema +export const deletePlatformInputSchema = deletePlatformDtoSchema + +export const listPlatformsInputSchema = z.object({ + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(10), + search: z.string().optional(), +}) + +export const platformOutputSchema = platformSimpleRoSchema + +export const listPlatformsOutputSchema = z.object({ + platforms: z.array(platformOutputSchema), + total: z.number().int(), + hasMore: z.boolean(), + page: z.number().int(), + limit: z.number().int(), +}) + +// Tag DTOs +export const createTagInputSchema = tagDtoSchema +export const getTagInputSchema = getTagByIdDtoSchema +export const updateTagInputSchema = updateTagDtoSchema +export const deleteTagInputSchema = deleteTagDtoSchema + +export const listTagsInputSchema = z.object({ + page: z.number().int().min(1).default(1), + limit: z.number().int().min(1).max(100).default(10), + search: z.string().optional(), + containerId: z.string().optional(), +}) + +export const tagOutputSchema = tagSimpleRoSchema + +export const listTagsOutputSchema = z.object({ + tags: z.array(tagOutputSchema), + total: z.number().int(), + hasMore: z.boolean(), + page: z.number().int(), + limit: z.number().int(), +}) + +// Export types +export type CreateContainerInput = z.infer +export type GetContainerInput = z.infer +export type UpdateContainerInput = z.infer +export type DeleteContainerInput = z.infer +export type ListContainersInput = z.infer + +export type ContainerOutput = z.infer +export type ListContainersOutput = z.infer + +export type CreatePlatformInput = z.infer +export type GetPlatformInput = z.infer +export type UpdatePlatformInput = z.infer +export type DeletePlatformInput = z.infer +export type ListPlatformsInput = z.infer + +export type PlatformOutput = z.infer +export type ListPlatformsOutput = z.infer + +export type CreateTagInput = z.infer +export type GetTagInput = z.infer +export type UpdateTagInput = z.infer +export type DeleteTagInput = z.infer +export type ListTagsInput = z.infer + +export type TagOutput = z.infer +export type ListTagsOutput = z.infer diff --git a/schemas/utils/index.ts b/schemas/utils/index.ts index 01a7fc2..c601077 100644 --- a/schemas/utils/index.ts +++ b/schemas/utils/index.ts @@ -1,4 +1,5 @@ export * from "./container" +export * from "./container-with-secrets" export * from "./utils" export * from "./tag" export * from "./platform"