Allow manager orders access without team claim
Build and deploy Docker image / build (push) Successful in 2m4s
Build and deploy Docker image / build (push) Successful in 2m4s
This commit is contained in:
+6
-2
@@ -116,12 +116,16 @@ export async function managerContext(
|
|||||||
const teamUuid = (payload as Record<string, unknown>).team_uuid as
|
const teamUuid = (payload as Record<string, unknown>).team_uuid as
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
if (!payload.sub || !hasManagerClaim(payload) || !teamUuid) {
|
if (!payload.sub || !hasManagerClaim(payload)) {
|
||||||
throw new GraphQLError("Unauthorized", {
|
throw new GraphQLError("Unauthorized", {
|
||||||
extensions: { code: "UNAUTHENTICATED" },
|
extensions: { code: "UNAUTHENTICATED" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { userId: payload.sub, teamUuid, scopes: ["teams:member", "manager"] };
|
return {
|
||||||
|
userId: payload.sub,
|
||||||
|
teamUuid,
|
||||||
|
scopes: [...new Set([...scopes, "manager"])],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireScopes(ctx: AuthContext, ...required: string[]): void {
|
export function requireScopes(ctx: AuthContext, ...required: string[]): void {
|
||||||
|
|||||||
+69
-35
@@ -401,7 +401,18 @@ function isoString(value: Date | null | undefined): string | null {
|
|||||||
return value ? value.toISOString() : null
|
return value ? value.toISOString() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertTeamAccess(ctx: AuthContext): { teamUuid: string; userId: string } {
|
function assertTeamAccess(ctx: AuthContext): { teamUuid?: string; userId: string; isManager: boolean } {
|
||||||
|
if (ctx.scopes.includes('manager')) {
|
||||||
|
if (!ctx.userId) {
|
||||||
|
throw new GraphQLError('User not authenticated')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
teamUuid: ctx.teamUuid,
|
||||||
|
userId: ctx.userId,
|
||||||
|
isManager: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
requireScopes(ctx, 'teams:member')
|
requireScopes(ctx, 'teams:member')
|
||||||
if (!ctx.teamUuid || !ctx.userId) {
|
if (!ctx.teamUuid || !ctx.userId) {
|
||||||
throw new GraphQLError('User not authenticated')
|
throw new GraphQLError('User not authenticated')
|
||||||
@@ -410,9 +421,25 @@ function assertTeamAccess(ctx: AuthContext): { teamUuid: string; userId: string
|
|||||||
return {
|
return {
|
||||||
teamUuid: ctx.teamUuid,
|
teamUuid: ctx.teamUuid,
|
||||||
userId: ctx.userId,
|
userId: ctx.userId,
|
||||||
|
isManager: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertScopedTeamAccess(ctx: AuthContext): { teamUuid: string; userId: string } {
|
||||||
|
const access = assertTeamAccess(ctx)
|
||||||
|
if (!access.teamUuid) {
|
||||||
|
throw new GraphQLError('Team context is required')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
userId: access.userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function teamAccessWhere(access: { teamUuid?: string }): { teamUuid?: string } {
|
||||||
|
return access.teamUuid ? { teamUuid: access.teamUuid } : {}
|
||||||
|
}
|
||||||
|
|
||||||
function mapTariffReference(item: PrismaTariffReference) {
|
function mapTariffReference(item: PrismaTariffReference) {
|
||||||
return {
|
return {
|
||||||
uuid: item.uuid,
|
uuid: item.uuid,
|
||||||
@@ -734,11 +761,11 @@ function computeTariffMatches(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadQuotationOrThrow(quotationUuid: string, teamUuid: string) {
|
async function loadQuotationOrThrow(quotationUuid: string, teamUuid?: string) {
|
||||||
const item = await prisma.quotation.findFirst({
|
const item = await prisma.quotation.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uuid: quotationUuid,
|
uuid: quotationUuid,
|
||||||
teamUuid,
|
...teamAccessWhere({ teamUuid }),
|
||||||
},
|
},
|
||||||
include: quotationInclude,
|
include: quotationInclude,
|
||||||
})
|
})
|
||||||
@@ -750,11 +777,11 @@ async function loadQuotationOrThrow(quotationUuid: string, teamUuid: string) {
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTariffOrThrow(tariffReferenceUuid: string, teamUuid: string) {
|
async function loadTariffOrThrow(tariffReferenceUuid: string, teamUuid?: string) {
|
||||||
const item = await prisma.tariffReference.findFirst({
|
const item = await prisma.tariffReference.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uuid: tariffReferenceUuid,
|
uuid: tariffReferenceUuid,
|
||||||
teamUuid,
|
...teamAccessWhere({ teamUuid }),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -882,11 +909,11 @@ async function refreshTeamQuotations(teamUuid: string, actor: { userId: string;
|
|||||||
export const teamResolvers = {
|
export const teamResolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
getTeamOrders: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
getTeamOrders: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
||||||
const { teamUuid } = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
|
|
||||||
const localOrders = await prisma.order.findMany({
|
const localOrders = await prisma.order.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamUuid,
|
...teamAccessWhere(access),
|
||||||
},
|
},
|
||||||
include: orderInclude,
|
include: orderInclude,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
@@ -898,12 +925,12 @@ export const teamResolvers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getOrder: async (_: unknown, args: { orderUuid: string }, ctx: AuthContext) => {
|
getOrder: async (_: unknown, args: { orderUuid: string }, ctx: AuthContext) => {
|
||||||
const { teamUuid } = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
|
|
||||||
const order = await prisma.order.findFirst({
|
const order = await prisma.order.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uuid: args.orderUuid,
|
uuid: args.orderUuid,
|
||||||
teamUuid,
|
...teamAccessWhere(access),
|
||||||
},
|
},
|
||||||
include: orderInclude,
|
include: orderInclude,
|
||||||
})
|
})
|
||||||
@@ -912,11 +939,11 @@ export const teamResolvers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
quotations: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => {
|
quotations: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => {
|
||||||
const { teamUuid } = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
|
|
||||||
const items = await prisma.quotation.findMany({
|
const items = await prisma.quotation.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamUuid,
|
...teamAccessWhere(access),
|
||||||
status: normalizeString(args.status) ?? undefined,
|
status: normalizeString(args.status) ?? undefined,
|
||||||
},
|
},
|
||||||
include: quotationInclude,
|
include: quotationInclude,
|
||||||
@@ -929,11 +956,11 @@ export const teamResolvers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
quotation: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
quotation: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
||||||
const { teamUuid } = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const item = await prisma.quotation.findFirst({
|
const item = await prisma.quotation.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uuid: args.quotationUuid,
|
uuid: args.quotationUuid,
|
||||||
teamUuid,
|
...teamAccessWhere(access),
|
||||||
},
|
},
|
||||||
include: quotationInclude,
|
include: quotationInclude,
|
||||||
})
|
})
|
||||||
@@ -941,11 +968,11 @@ export const teamResolvers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
tariffReferences: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => {
|
tariffReferences: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => {
|
||||||
const { teamUuid } = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
|
|
||||||
const items = await prisma.tariffReference.findMany({
|
const items = await prisma.tariffReference.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamUuid,
|
...teamAccessWhere(access),
|
||||||
status: normalizeString(args.status) ?? undefined,
|
status: normalizeString(args.status) ?? undefined,
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
@@ -958,22 +985,22 @@ export const teamResolvers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
tariffReference: async (_: unknown, args: { tariffReferenceUuid: string }, ctx: AuthContext) => {
|
tariffReference: async (_: unknown, args: { tariffReferenceUuid: string }, ctx: AuthContext) => {
|
||||||
const { teamUuid } = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const item = await prisma.tariffReference.findFirst({
|
const item = await prisma.tariffReference.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uuid: args.tariffReferenceUuid,
|
uuid: args.tariffReferenceUuid,
|
||||||
teamUuid,
|
...teamAccessWhere(access),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return item ? mapTariffReference(item) : null
|
return item ? mapTariffReference(item) : null
|
||||||
},
|
},
|
||||||
|
|
||||||
quotationTariffMatches: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
quotationTariffMatches: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
||||||
const { teamUuid } = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const quotation = await loadQuotationOrThrow(args.quotationUuid, teamUuid)
|
const quotation = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||||
const tariffs = await prisma.tariffReference.findMany({
|
const tariffs = await prisma.tariffReference.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamUuid,
|
teamUuid: quotation.teamUuid,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -992,7 +1019,7 @@ export const teamResolvers = {
|
|||||||
|
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createTariffReference: async (_: unknown, args: { input: CreateTariffReferenceInput }, ctx: AuthContext) => {
|
createTariffReference: async (_: unknown, args: { input: CreateTariffReferenceInput }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertScopedTeamAccess(ctx)
|
||||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||||
const item = await prisma.tariffReference.create({
|
const item = await prisma.tariffReference.create({
|
||||||
data: buildCreateTariffData(args.input, access),
|
data: buildCreateTariffData(args.input, access),
|
||||||
@@ -1003,33 +1030,35 @@ export const teamResolvers = {
|
|||||||
|
|
||||||
updateTariffReference: async (_: unknown, args: { tariffReferenceUuid: string; input: UpdateTariffReferenceInput }, ctx: AuthContext) => {
|
updateTariffReference: async (_: unknown, args: { tariffReferenceUuid: string; input: UpdateTariffReferenceInput }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
|
||||||
const item = await loadTariffOrThrow(args.tariffReferenceUuid, access.teamUuid)
|
const item = await loadTariffOrThrow(args.tariffReferenceUuid, access.teamUuid)
|
||||||
|
const teamAccess = { teamUuid: item.teamUuid, userId: access.userId }
|
||||||
|
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||||
const updated = await prisma.tariffReference.update({
|
const updated = await prisma.tariffReference.update({
|
||||||
where: {
|
where: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
},
|
},
|
||||||
data: buildUpdateTariffData(args.input),
|
data: buildUpdateTariffData(args.input),
|
||||||
})
|
})
|
||||||
await refreshTeamQuotations(access.teamUuid, actor)
|
await refreshTeamQuotations(item.teamUuid, actor)
|
||||||
return mapTariffReference(updated)
|
return mapTariffReference(updated)
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTariffReference: async (_: unknown, args: { tariffReferenceUuid: string }, ctx: AuthContext) => {
|
deleteTariffReference: async (_: unknown, args: { tariffReferenceUuid: string }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
|
||||||
const item = await loadTariffOrThrow(args.tariffReferenceUuid, access.teamUuid)
|
const item = await loadTariffOrThrow(args.tariffReferenceUuid, access.teamUuid)
|
||||||
|
const teamAccess = { teamUuid: item.teamUuid, userId: access.userId }
|
||||||
|
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||||
await prisma.tariffReference.delete({
|
await prisma.tariffReference.delete({
|
||||||
where: {
|
where: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await refreshTeamQuotations(access.teamUuid, actor)
|
await refreshTeamQuotations(item.teamUuid, actor)
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
createQuotation: async (_: unknown, args: { input: CreateQuotationInput }, ctx: AuthContext) => {
|
createQuotation: async (_: unknown, args: { input: CreateQuotationInput }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertScopedTeamAccess(ctx)
|
||||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
const actor = { userId: access.userId, label: actorLabel(access) }
|
||||||
const created = await prisma.quotation.create({
|
const created = await prisma.quotation.create({
|
||||||
data: buildCreateQuotationData(args.input, access),
|
data: buildCreateQuotationData(args.input, access),
|
||||||
@@ -1045,8 +1074,9 @@ export const teamResolvers = {
|
|||||||
|
|
||||||
updateQuotation: async (_: unknown, args: { quotationUuid: string; input: UpdateQuotationInput }, ctx: AuthContext) => {
|
updateQuotation: async (_: unknown, args: { quotationUuid: string; input: UpdateQuotationInput }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
|
||||||
const current = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
const current = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||||
|
const teamAccess = { teamUuid: current.teamUuid, userId: access.userId }
|
||||||
|
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||||
await prisma.quotation.update({
|
await prisma.quotation.update({
|
||||||
where: {
|
where: {
|
||||||
id: current.id,
|
id: current.id,
|
||||||
@@ -1059,22 +1089,25 @@ export const teamResolvers = {
|
|||||||
updatedFields: Object.keys(args.input),
|
updatedFields: Object.keys(args.input),
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshed = await refreshQuotationSelection(current.uuid, access.teamUuid, actor)
|
const refreshed = await refreshQuotationSelection(current.uuid, current.teamUuid, actor)
|
||||||
return mapQuotation(refreshed)
|
return mapQuotation(refreshed)
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshQuotationTariff: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
refreshQuotationTariff: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
const current = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||||
const refreshed = await refreshQuotationSelection(args.quotationUuid, access.teamUuid, actor)
|
const teamAccess = { teamUuid: current.teamUuid, userId: access.userId }
|
||||||
|
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||||
|
const refreshed = await refreshQuotationSelection(current.uuid, current.teamUuid, actor)
|
||||||
return mapQuotation(refreshed)
|
return mapQuotation(refreshed)
|
||||||
},
|
},
|
||||||
|
|
||||||
selectQuotationTariff: async (_: unknown, args: { quotationUuid: string; tariffReferenceUuid: string }, ctx: AuthContext) => {
|
selectQuotationTariff: async (_: unknown, args: { quotationUuid: string; tariffReferenceUuid: string }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
|
||||||
const quotation = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
const quotation = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||||
const tariff = await loadTariffOrThrow(args.tariffReferenceUuid, access.teamUuid)
|
const teamAccess = { teamUuid: quotation.teamUuid, userId: access.userId }
|
||||||
|
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||||
|
const tariff = await loadTariffOrThrow(args.tariffReferenceUuid, quotation.teamUuid)
|
||||||
|
|
||||||
const projectedAmount = Math.max(Number(tariff.amountUsd), numberFromDecimal(tariff.minPriceUsd) ?? 0)
|
const projectedAmount = Math.max(Number(tariff.amountUsd), numberFromDecimal(tariff.minPriceUsd) ?? 0)
|
||||||
await prisma.quotation.update({
|
await prisma.quotation.update({
|
||||||
@@ -1107,13 +1140,14 @@ export const teamResolvers = {
|
|||||||
|
|
||||||
createOrderFromQuotation: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
createOrderFromQuotation: async (_: unknown, args: { quotationUuid: string }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const actor = { userId: access.userId, label: actorLabel(access) }
|
|
||||||
const quotation = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
const quotation = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
||||||
|
const teamAccess = { teamUuid: quotation.teamUuid, userId: access.userId }
|
||||||
|
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||||
|
|
||||||
const existingOrder = await prisma.order.findFirst({
|
const existingOrder = await prisma.order.findFirst({
|
||||||
where: {
|
where: {
|
||||||
quotationId: quotation.id,
|
quotationId: quotation.id,
|
||||||
teamUuid: access.teamUuid,
|
teamUuid: quotation.teamUuid,
|
||||||
},
|
},
|
||||||
include: orderInclude,
|
include: orderInclude,
|
||||||
})
|
})
|
||||||
@@ -1128,7 +1162,7 @@ export const teamResolvers = {
|
|||||||
|
|
||||||
const order = await prisma.order.create({
|
const order = await prisma.order.create({
|
||||||
data: {
|
data: {
|
||||||
teamUuid: access.teamUuid,
|
teamUuid: quotation.teamUuid,
|
||||||
quotationId: quotation.id,
|
quotationId: quotation.id,
|
||||||
createdByUserId: access.userId,
|
createdByUserId: access.userId,
|
||||||
name: quotation.title.startsWith('Order') ? quotation.title : `Order for ${quotation.title}`,
|
name: quotation.title.startsWith('Order') ? quotation.title : `Order for ${quotation.title}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user