Projektstart
This commit is contained in:
@@ -45,7 +45,8 @@ docker compose up --build
|
|||||||
- `POST /admin/jobs/:id/retry` (admin)
|
- `POST /admin/jobs/:id/retry` (admin)
|
||||||
- `POST /admin/impersonate/:userId` (admin)
|
- `POST /admin/impersonate/:userId` (admin)
|
||||||
- `GET /admin/tenants/:id/export` (admin)
|
- `GET /admin/tenants/:id/export` (admin)
|
||||||
- `GET /admin/tenants/:id/export?scope=users|accounts|jobs|rules&format=csv|zip` (admin, zip bundle)
|
- `GET /admin/tenants/:id/export?scope=users|accounts|jobs|rules&format=csv|zip` (admin, zip returns jobId)
|
||||||
|
- `GET /admin/exports` (admin)
|
||||||
- `GET /admin/exports/:id` (admin)
|
- `GET /admin/exports/:id` (admin)
|
||||||
- `GET /admin/exports/:id/download` (admin)
|
- `GET /admin/exports/:id/download` (admin)
|
||||||
- `GET /jobs/exports/:id/stream` (auth, SSE)
|
- `GET /jobs/exports/:id/stream` (auth, SSE)
|
||||||
@@ -77,3 +78,7 @@ npm run prisma:seed
|
|||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
`docker-compose.yml` sets default dev credentials. Adjust before production use.
|
`docker-compose.yml` sets default dev credentials. Adjust before production use.
|
||||||
|
|
||||||
|
Export settings:
|
||||||
|
- `EXPORT_DIR` (default `/tmp/mailcleaner-exports`)
|
||||||
|
- `EXPORT_TTL_HOURS` (default `24`)
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ JWT_SECRET=change-me-super-secret
|
|||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
GOOGLE_REDIRECT_URI=http://localhost:8000/oauth/gmail/callback
|
GOOGLE_REDIRECT_URI=http://localhost:8000/oauth/gmail/callback
|
||||||
|
EXPORT_DIR=/tmp/mailcleaner-exports
|
||||||
|
EXPORT_TTL_HOURS=24
|
||||||
|
|||||||
7
backend/node_modules/.prisma/client/edge.js
generated
vendored
7
backend/node_modules/.prisma/client/edge.js
generated
vendored
File diff suppressed because one or more lines are too long
1
backend/node_modules/.prisma/client/index-browser.js
generated
vendored
1
backend/node_modules/.prisma/client/index-browser.js
generated
vendored
@@ -138,6 +138,7 @@ exports.Prisma.ExportJobScalarFieldEnum = {
|
|||||||
scope: 'scope',
|
scope: 'scope',
|
||||||
filePath: 'filePath',
|
filePath: 'filePath',
|
||||||
error: 'error',
|
error: 'error',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|||||||
143
backend/node_modules/.prisma/client/index.d.ts
generated
vendored
143
backend/node_modules/.prisma/client/index.d.ts
generated
vendored
@@ -3156,6 +3156,7 @@ export namespace Prisma {
|
|||||||
scope: string | null
|
scope: string | null
|
||||||
filePath: string | null
|
filePath: string | null
|
||||||
error: string | null
|
error: string | null
|
||||||
|
expiresAt: Date | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -3168,6 +3169,7 @@ export namespace Prisma {
|
|||||||
scope: string | null
|
scope: string | null
|
||||||
filePath: string | null
|
filePath: string | null
|
||||||
error: string | null
|
error: string | null
|
||||||
|
expiresAt: Date | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -3180,6 +3182,7 @@ export namespace Prisma {
|
|||||||
scope: number
|
scope: number
|
||||||
filePath: number
|
filePath: number
|
||||||
error: number
|
error: number
|
||||||
|
expiresAt: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
_all: number
|
_all: number
|
||||||
@@ -3194,6 +3197,7 @@ export namespace Prisma {
|
|||||||
scope?: true
|
scope?: true
|
||||||
filePath?: true
|
filePath?: true
|
||||||
error?: true
|
error?: true
|
||||||
|
expiresAt?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -3206,6 +3210,7 @@ export namespace Prisma {
|
|||||||
scope?: true
|
scope?: true
|
||||||
filePath?: true
|
filePath?: true
|
||||||
error?: true
|
error?: true
|
||||||
|
expiresAt?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -3218,6 +3223,7 @@ export namespace Prisma {
|
|||||||
scope?: true
|
scope?: true
|
||||||
filePath?: true
|
filePath?: true
|
||||||
error?: true
|
error?: true
|
||||||
|
expiresAt?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
@@ -3303,6 +3309,7 @@ export namespace Prisma {
|
|||||||
scope: string
|
scope: string
|
||||||
filePath: string | null
|
filePath: string | null
|
||||||
error: string | null
|
error: string | null
|
||||||
|
expiresAt: Date | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
_count: ExportJobCountAggregateOutputType | null
|
_count: ExportJobCountAggregateOutputType | null
|
||||||
@@ -3332,6 +3339,7 @@ export namespace Prisma {
|
|||||||
scope?: boolean
|
scope?: boolean
|
||||||
filePath?: boolean
|
filePath?: boolean
|
||||||
error?: boolean
|
error?: boolean
|
||||||
|
expiresAt?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
tenant?: boolean | TenantDefaultArgs<ExtArgs>
|
tenant?: boolean | TenantDefaultArgs<ExtArgs>
|
||||||
@@ -3345,6 +3353,7 @@ export namespace Prisma {
|
|||||||
scope?: boolean
|
scope?: boolean
|
||||||
filePath?: boolean
|
filePath?: boolean
|
||||||
error?: boolean
|
error?: boolean
|
||||||
|
expiresAt?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
tenant?: boolean | TenantDefaultArgs<ExtArgs>
|
tenant?: boolean | TenantDefaultArgs<ExtArgs>
|
||||||
@@ -3358,6 +3367,7 @@ export namespace Prisma {
|
|||||||
scope?: boolean
|
scope?: boolean
|
||||||
filePath?: boolean
|
filePath?: boolean
|
||||||
error?: boolean
|
error?: boolean
|
||||||
|
expiresAt?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
@@ -3382,6 +3392,7 @@ export namespace Prisma {
|
|||||||
scope: string
|
scope: string
|
||||||
filePath: string | null
|
filePath: string | null
|
||||||
error: string | null
|
error: string | null
|
||||||
|
expiresAt: Date | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}, ExtArgs["result"]["exportJob"]>
|
}, ExtArgs["result"]["exportJob"]>
|
||||||
@@ -3785,6 +3796,7 @@ export namespace Prisma {
|
|||||||
readonly scope: FieldRef<"ExportJob", 'String'>
|
readonly scope: FieldRef<"ExportJob", 'String'>
|
||||||
readonly filePath: FieldRef<"ExportJob", 'String'>
|
readonly filePath: FieldRef<"ExportJob", 'String'>
|
||||||
readonly error: FieldRef<"ExportJob", 'String'>
|
readonly error: FieldRef<"ExportJob", 'String'>
|
||||||
|
readonly expiresAt: FieldRef<"ExportJob", 'DateTime'>
|
||||||
readonly createdAt: FieldRef<"ExportJob", 'DateTime'>
|
readonly createdAt: FieldRef<"ExportJob", 'DateTime'>
|
||||||
readonly updatedAt: FieldRef<"ExportJob", 'DateTime'>
|
readonly updatedAt: FieldRef<"ExportJob", 'DateTime'>
|
||||||
}
|
}
|
||||||
@@ -14108,6 +14120,7 @@ export namespace Prisma {
|
|||||||
scope: 'scope',
|
scope: 'scope',
|
||||||
filePath: 'filePath',
|
filePath: 'filePath',
|
||||||
error: 'error',
|
error: 'error',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
@@ -14513,6 +14526,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFilter<"ExportJob"> | string
|
scope?: StringFilter<"ExportJob"> | string
|
||||||
filePath?: StringNullableFilter<"ExportJob"> | string | null
|
filePath?: StringNullableFilter<"ExportJob"> | string | null
|
||||||
error?: StringNullableFilter<"ExportJob"> | string | null
|
error?: StringNullableFilter<"ExportJob"> | string | null
|
||||||
|
expiresAt?: DateTimeNullableFilter<"ExportJob"> | Date | string | null
|
||||||
createdAt?: DateTimeFilter<"ExportJob"> | Date | string
|
createdAt?: DateTimeFilter<"ExportJob"> | Date | string
|
||||||
updatedAt?: DateTimeFilter<"ExportJob"> | Date | string
|
updatedAt?: DateTimeFilter<"ExportJob"> | Date | string
|
||||||
tenant?: XOR<TenantRelationFilter, TenantWhereInput>
|
tenant?: XOR<TenantRelationFilter, TenantWhereInput>
|
||||||
@@ -14526,6 +14540,7 @@ export namespace Prisma {
|
|||||||
scope?: SortOrder
|
scope?: SortOrder
|
||||||
filePath?: SortOrderInput | SortOrder
|
filePath?: SortOrderInput | SortOrder
|
||||||
error?: SortOrderInput | SortOrder
|
error?: SortOrderInput | SortOrder
|
||||||
|
expiresAt?: SortOrderInput | SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
updatedAt?: SortOrder
|
updatedAt?: SortOrder
|
||||||
tenant?: TenantOrderByWithRelationInput
|
tenant?: TenantOrderByWithRelationInput
|
||||||
@@ -14542,6 +14557,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFilter<"ExportJob"> | string
|
scope?: StringFilter<"ExportJob"> | string
|
||||||
filePath?: StringNullableFilter<"ExportJob"> | string | null
|
filePath?: StringNullableFilter<"ExportJob"> | string | null
|
||||||
error?: StringNullableFilter<"ExportJob"> | string | null
|
error?: StringNullableFilter<"ExportJob"> | string | null
|
||||||
|
expiresAt?: DateTimeNullableFilter<"ExportJob"> | Date | string | null
|
||||||
createdAt?: DateTimeFilter<"ExportJob"> | Date | string
|
createdAt?: DateTimeFilter<"ExportJob"> | Date | string
|
||||||
updatedAt?: DateTimeFilter<"ExportJob"> | Date | string
|
updatedAt?: DateTimeFilter<"ExportJob"> | Date | string
|
||||||
tenant?: XOR<TenantRelationFilter, TenantWhereInput>
|
tenant?: XOR<TenantRelationFilter, TenantWhereInput>
|
||||||
@@ -14555,6 +14571,7 @@ export namespace Prisma {
|
|||||||
scope?: SortOrder
|
scope?: SortOrder
|
||||||
filePath?: SortOrderInput | SortOrder
|
filePath?: SortOrderInput | SortOrder
|
||||||
error?: SortOrderInput | SortOrder
|
error?: SortOrderInput | SortOrder
|
||||||
|
expiresAt?: SortOrderInput | SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
updatedAt?: SortOrder
|
updatedAt?: SortOrder
|
||||||
_count?: ExportJobCountOrderByAggregateInput
|
_count?: ExportJobCountOrderByAggregateInput
|
||||||
@@ -14573,6 +14590,7 @@ export namespace Prisma {
|
|||||||
scope?: StringWithAggregatesFilter<"ExportJob"> | string
|
scope?: StringWithAggregatesFilter<"ExportJob"> | string
|
||||||
filePath?: StringNullableWithAggregatesFilter<"ExportJob"> | string | null
|
filePath?: StringNullableWithAggregatesFilter<"ExportJob"> | string | null
|
||||||
error?: StringNullableWithAggregatesFilter<"ExportJob"> | string | null
|
error?: StringNullableWithAggregatesFilter<"ExportJob"> | string | null
|
||||||
|
expiresAt?: DateTimeNullableWithAggregatesFilter<"ExportJob"> | Date | string | null
|
||||||
createdAt?: DateTimeWithAggregatesFilter<"ExportJob"> | Date | string
|
createdAt?: DateTimeWithAggregatesFilter<"ExportJob"> | Date | string
|
||||||
updatedAt?: DateTimeWithAggregatesFilter<"ExportJob"> | Date | string
|
updatedAt?: DateTimeWithAggregatesFilter<"ExportJob"> | Date | string
|
||||||
}
|
}
|
||||||
@@ -15393,6 +15411,7 @@ export namespace Prisma {
|
|||||||
scope: string
|
scope: string
|
||||||
filePath?: string | null
|
filePath?: string | null
|
||||||
error?: string | null
|
error?: string | null
|
||||||
|
expiresAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
tenant: TenantCreateNestedOneWithoutExportJobsInput
|
tenant: TenantCreateNestedOneWithoutExportJobsInput
|
||||||
@@ -15406,6 +15425,7 @@ export namespace Prisma {
|
|||||||
scope: string
|
scope: string
|
||||||
filePath?: string | null
|
filePath?: string | null
|
||||||
error?: string | null
|
error?: string | null
|
||||||
|
expiresAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -15417,6 +15437,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFieldUpdateOperationsInput | string
|
scope?: StringFieldUpdateOperationsInput | string
|
||||||
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
error?: NullableStringFieldUpdateOperationsInput | string | null
|
error?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
tenant?: TenantUpdateOneRequiredWithoutExportJobsNestedInput
|
tenant?: TenantUpdateOneRequiredWithoutExportJobsNestedInput
|
||||||
@@ -15430,6 +15451,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFieldUpdateOperationsInput | string
|
scope?: StringFieldUpdateOperationsInput | string
|
||||||
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
error?: NullableStringFieldUpdateOperationsInput | string | null
|
error?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -15442,6 +15464,7 @@ export namespace Prisma {
|
|||||||
scope: string
|
scope: string
|
||||||
filePath?: string | null
|
filePath?: string | null
|
||||||
error?: string | null
|
error?: string | null
|
||||||
|
expiresAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -15453,6 +15476,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFieldUpdateOperationsInput | string
|
scope?: StringFieldUpdateOperationsInput | string
|
||||||
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
error?: NullableStringFieldUpdateOperationsInput | string | null
|
error?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -15465,6 +15489,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFieldUpdateOperationsInput | string
|
scope?: StringFieldUpdateOperationsInput | string
|
||||||
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
error?: NullableStringFieldUpdateOperationsInput | string | null
|
error?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -16430,6 +16455,17 @@ export namespace Prisma {
|
|||||||
not?: NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type TenantRelationFilter = {
|
export type TenantRelationFilter = {
|
||||||
is?: TenantWhereInput
|
is?: TenantWhereInput
|
||||||
isNot?: TenantWhereInput
|
isNot?: TenantWhereInput
|
||||||
@@ -16448,6 +16484,7 @@ export namespace Prisma {
|
|||||||
scope?: SortOrder
|
scope?: SortOrder
|
||||||
filePath?: SortOrder
|
filePath?: SortOrder
|
||||||
error?: SortOrder
|
error?: SortOrder
|
||||||
|
expiresAt?: SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
updatedAt?: SortOrder
|
updatedAt?: SortOrder
|
||||||
}
|
}
|
||||||
@@ -16460,6 +16497,7 @@ export namespace Prisma {
|
|||||||
scope?: SortOrder
|
scope?: SortOrder
|
||||||
filePath?: SortOrder
|
filePath?: SortOrder
|
||||||
error?: SortOrder
|
error?: SortOrder
|
||||||
|
expiresAt?: SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
updatedAt?: SortOrder
|
updatedAt?: SortOrder
|
||||||
}
|
}
|
||||||
@@ -16472,6 +16510,7 @@ export namespace Prisma {
|
|||||||
scope?: SortOrder
|
scope?: SortOrder
|
||||||
filePath?: SortOrder
|
filePath?: SortOrder
|
||||||
error?: SortOrder
|
error?: SortOrder
|
||||||
|
expiresAt?: SortOrder
|
||||||
createdAt?: SortOrder
|
createdAt?: SortOrder
|
||||||
updatedAt?: SortOrder
|
updatedAt?: SortOrder
|
||||||
}
|
}
|
||||||
@@ -16504,6 +16543,20 @@ export namespace Prisma {
|
|||||||
_max?: NestedStringNullableFilter<$PrismaModel>
|
_max?: NestedStringNullableFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type EnumUserRoleFilter<$PrismaModel = never> = {
|
export type EnumUserRoleFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.UserRole | EnumUserRoleFieldRefInput<$PrismaModel>
|
equals?: $Enums.UserRole | EnumUserRoleFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.UserRole[] | ListEnumUserRoleFieldRefInput<$PrismaModel>
|
in?: $Enums.UserRole[] | ListEnumUserRoleFieldRefInput<$PrismaModel>
|
||||||
@@ -16588,17 +16641,6 @@ export namespace Prisma {
|
|||||||
not?: NestedBoolNullableFilter<$PrismaModel> | boolean | null
|
not?: NestedBoolNullableFilter<$PrismaModel> | boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
|
||||||
equals?: Date | string | DateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
notIn?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
lt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
lte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
not?: NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MailboxFolderListRelationFilter = {
|
export type MailboxFolderListRelationFilter = {
|
||||||
every?: MailboxFolderWhereInput
|
every?: MailboxFolderWhereInput
|
||||||
some?: MailboxFolderWhereInput
|
some?: MailboxFolderWhereInput
|
||||||
@@ -16735,20 +16777,6 @@ export namespace Prisma {
|
|||||||
_max?: NestedBoolNullableFilter<$PrismaModel>
|
_max?: NestedBoolNullableFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: Date | string | DateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
notIn?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
lt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
lte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
not?: NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
|
||||||
_count?: NestedIntNullableFilter<$PrismaModel>
|
|
||||||
_min?: NestedDateTimeNullableFilter<$PrismaModel>
|
|
||||||
_max?: NestedDateTimeNullableFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MailboxAccountRelationFilter = {
|
export type MailboxAccountRelationFilter = {
|
||||||
is?: MailboxAccountWhereInput
|
is?: MailboxAccountWhereInput
|
||||||
isNot?: MailboxAccountWhereInput
|
isNot?: MailboxAccountWhereInput
|
||||||
@@ -17348,6 +17376,10 @@ export namespace Prisma {
|
|||||||
set?: string | null
|
set?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NullableDateTimeFieldUpdateOperationsInput = {
|
||||||
|
set?: Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type TenantUpdateOneRequiredWithoutExportJobsNestedInput = {
|
export type TenantUpdateOneRequiredWithoutExportJobsNestedInput = {
|
||||||
create?: XOR<TenantCreateWithoutExportJobsInput, TenantUncheckedCreateWithoutExportJobsInput>
|
create?: XOR<TenantCreateWithoutExportJobsInput, TenantUncheckedCreateWithoutExportJobsInput>
|
||||||
connectOrCreate?: TenantCreateOrConnectWithoutExportJobsInput
|
connectOrCreate?: TenantCreateOrConnectWithoutExportJobsInput
|
||||||
@@ -17432,10 +17464,6 @@ export namespace Prisma {
|
|||||||
set?: boolean | null
|
set?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NullableDateTimeFieldUpdateOperationsInput = {
|
|
||||||
set?: Date | string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TenantUpdateOneRequiredWithoutMailboxAccountsNestedInput = {
|
export type TenantUpdateOneRequiredWithoutMailboxAccountsNestedInput = {
|
||||||
create?: XOR<TenantCreateWithoutMailboxAccountsInput, TenantUncheckedCreateWithoutMailboxAccountsInput>
|
create?: XOR<TenantCreateWithoutMailboxAccountsInput, TenantUncheckedCreateWithoutMailboxAccountsInput>
|
||||||
connectOrCreate?: TenantCreateOrConnectWithoutMailboxAccountsInput
|
connectOrCreate?: TenantCreateOrConnectWithoutMailboxAccountsInput
|
||||||
@@ -17949,6 +17977,17 @@ export namespace Prisma {
|
|||||||
not?: NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedEnumExportStatusWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedEnumExportStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.ExportStatus | EnumExportStatusFieldRefInput<$PrismaModel>
|
equals?: $Enums.ExportStatus | EnumExportStatusFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.ExportStatus[] | ListEnumExportStatusFieldRefInput<$PrismaModel>
|
in?: $Enums.ExportStatus[] | ListEnumExportStatusFieldRefInput<$PrismaModel>
|
||||||
@@ -17987,6 +18026,20 @@ export namespace Prisma {
|
|||||||
not?: NestedIntNullableFilter<$PrismaModel> | number | null
|
not?: NestedIntNullableFilter<$PrismaModel> | number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedEnumUserRoleFilter<$PrismaModel = never> = {
|
export type NestedEnumUserRoleFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.UserRole | EnumUserRoleFieldRefInput<$PrismaModel>
|
equals?: $Enums.UserRole | EnumUserRoleFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.UserRole[] | ListEnumUserRoleFieldRefInput<$PrismaModel>
|
in?: $Enums.UserRole[] | ListEnumUserRoleFieldRefInput<$PrismaModel>
|
||||||
@@ -18016,17 +18069,6 @@ export namespace Prisma {
|
|||||||
not?: NestedBoolNullableFilter<$PrismaModel> | boolean | null
|
not?: NestedBoolNullableFilter<$PrismaModel> | boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
|
||||||
equals?: Date | string | DateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
notIn?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
lt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
lte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
not?: NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedEnumMailProviderWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedEnumMailProviderWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.MailProvider | EnumMailProviderFieldRefInput<$PrismaModel>
|
equals?: $Enums.MailProvider | EnumMailProviderFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.MailProvider[] | ListEnumMailProviderFieldRefInput<$PrismaModel>
|
in?: $Enums.MailProvider[] | ListEnumMailProviderFieldRefInput<$PrismaModel>
|
||||||
@@ -18099,20 +18141,6 @@ export namespace Prisma {
|
|||||||
_max?: NestedBoolNullableFilter<$PrismaModel>
|
_max?: NestedBoolNullableFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: Date | string | DateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
notIn?: Date[] | string[] | ListDateTimeFieldRefInput<$PrismaModel> | null
|
|
||||||
lt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
lte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gt?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
gte?: Date | string | DateTimeFieldRefInput<$PrismaModel>
|
|
||||||
not?: NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
|
||||||
_count?: NestedIntNullableFilter<$PrismaModel>
|
|
||||||
_min?: NestedDateTimeNullableFilter<$PrismaModel>
|
|
||||||
_max?: NestedDateTimeNullableFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedEnumRuleConditionTypeFilter<$PrismaModel = never> = {
|
export type NestedEnumRuleConditionTypeFilter<$PrismaModel = never> = {
|
||||||
equals?: $Enums.RuleConditionType | EnumRuleConditionTypeFieldRefInput<$PrismaModel>
|
equals?: $Enums.RuleConditionType | EnumRuleConditionTypeFieldRefInput<$PrismaModel>
|
||||||
in?: $Enums.RuleConditionType[] | ListEnumRuleConditionTypeFieldRefInput<$PrismaModel>
|
in?: $Enums.RuleConditionType[] | ListEnumRuleConditionTypeFieldRefInput<$PrismaModel>
|
||||||
@@ -18171,6 +18199,7 @@ export namespace Prisma {
|
|||||||
scope: string
|
scope: string
|
||||||
filePath?: string | null
|
filePath?: string | null
|
||||||
error?: string | null
|
error?: string | null
|
||||||
|
expiresAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -18182,6 +18211,7 @@ export namespace Prisma {
|
|||||||
scope: string
|
scope: string
|
||||||
filePath?: string | null
|
filePath?: string | null
|
||||||
error?: string | null
|
error?: string | null
|
||||||
|
expiresAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -18379,6 +18409,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFilter<"ExportJob"> | string
|
scope?: StringFilter<"ExportJob"> | string
|
||||||
filePath?: StringNullableFilter<"ExportJob"> | string | null
|
filePath?: StringNullableFilter<"ExportJob"> | string | null
|
||||||
error?: StringNullableFilter<"ExportJob"> | string | null
|
error?: StringNullableFilter<"ExportJob"> | string | null
|
||||||
|
expiresAt?: DateTimeNullableFilter<"ExportJob"> | Date | string | null
|
||||||
createdAt?: DateTimeFilter<"ExportJob"> | Date | string
|
createdAt?: DateTimeFilter<"ExportJob"> | Date | string
|
||||||
updatedAt?: DateTimeFilter<"ExportJob"> | Date | string
|
updatedAt?: DateTimeFilter<"ExportJob"> | Date | string
|
||||||
}
|
}
|
||||||
@@ -19759,6 +19790,7 @@ export namespace Prisma {
|
|||||||
scope: string
|
scope: string
|
||||||
filePath?: string | null
|
filePath?: string | null
|
||||||
error?: string | null
|
error?: string | null
|
||||||
|
expiresAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -19822,6 +19854,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFieldUpdateOperationsInput | string
|
scope?: StringFieldUpdateOperationsInput | string
|
||||||
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
error?: NullableStringFieldUpdateOperationsInput | string | null
|
error?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -19833,6 +19866,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFieldUpdateOperationsInput | string
|
scope?: StringFieldUpdateOperationsInput | string
|
||||||
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
error?: NullableStringFieldUpdateOperationsInput | string | null
|
error?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -19844,6 +19878,7 @@ export namespace Prisma {
|
|||||||
scope?: StringFieldUpdateOperationsInput | string
|
scope?: StringFieldUpdateOperationsInput | string
|
||||||
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
filePath?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
error?: NullableStringFieldUpdateOperationsInput | string | null
|
error?: NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
|
|||||||
7
backend/node_modules/.prisma/client/index.js
generated
vendored
7
backend/node_modules/.prisma/client/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
backend/node_modules/.prisma/client/package.json
generated
vendored
2
backend/node_modules/.prisma/client/package.json
generated
vendored
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-f972eed39fc4466cb85f0349f5e72ee1d81f9351e0ae96befde50b28d520b85a",
|
"name": "prisma-client-e73c910a086b88f8fcb9eefa67d9d9c4f84c5390da0c21a4dde9378c21e5e10d",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "index-browser.js",
|
"browser": "index-browser.js",
|
||||||
|
|||||||
1
backend/node_modules/.prisma/client/schema.prisma
generated
vendored
1
backend/node_modules/.prisma/client/schema.prisma
generated
vendored
@@ -70,6 +70,7 @@ model ExportJob {
|
|||||||
scope String
|
scope String
|
||||||
filePath String?
|
filePath String?
|
||||||
error String?
|
error String?
|
||||||
|
expiresAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
1
backend/node_modules/.prisma/client/wasm.js
generated
vendored
1
backend/node_modules/.prisma/client/wasm.js
generated
vendored
@@ -138,6 +138,7 @@ exports.Prisma.ExportJobScalarFieldEnum = {
|
|||||||
scope: 'scope',
|
scope: 'scope',
|
||||||
filePath: 'filePath',
|
filePath: 'filePath',
|
||||||
error: 'error',
|
error: 'error',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ExportJob" ADD COLUMN "expiresAt" TIMESTAMP(3);
|
||||||
20
backend/src/admin/exportCleanup.ts
Normal file
20
backend/src/admin/exportCleanup.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { prisma } from "../db.js";
|
||||||
|
import { unlink } from "node:fs/promises";
|
||||||
|
|
||||||
|
export const cleanupExpiredExports = async () => {
|
||||||
|
const expired = await prisma.exportJob.findMany({
|
||||||
|
where: { expiresAt: { lt: new Date() } }
|
||||||
|
});
|
||||||
|
if (expired.length === 0) return;
|
||||||
|
|
||||||
|
for (const job of expired) {
|
||||||
|
if (job.filePath) {
|
||||||
|
try {
|
||||||
|
await unlink(job.filePath);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prisma.exportJob.delete({ where: { id: job.id } });
|
||||||
|
}
|
||||||
|
};
|
||||||
7
backend/src/admin/exportList.ts
Normal file
7
backend/src/admin/exportList.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { prisma } from "../db.js";
|
||||||
|
|
||||||
|
export const listExportsForTenant = async () => {
|
||||||
|
return prisma.exportJob.findMany({
|
||||||
|
orderBy: { createdAt: "desc" }
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { prisma } from "../db.js";
|
import { prisma } from "../db.js";
|
||||||
import { createExportArchive } from "./exportZip.js";
|
import { createExportArchive } from "./exportZip.js";
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { cleanupExpiredExports } from "./exportCleanup.js";
|
||||||
import { createWriteStream } from "node:fs";
|
import { createWriteStream } from "node:fs";
|
||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
|
|
||||||
const EXPORT_DIR = process.env.EXPORT_DIR ?? "/tmp/mailcleaner-exports";
|
const EXPORT_DIR = process.env.EXPORT_DIR ?? "/tmp/mailcleaner-exports";
|
||||||
|
const EXPORT_TTL_HOURS = Number(process.env.EXPORT_TTL_HOURS ?? 24);
|
||||||
|
|
||||||
const buildExport = async (jobId: string) => {
|
const buildExport = async (jobId: string) => {
|
||||||
const job = await prisma.exportJob.findUnique({ where: { id: jobId }, include: { tenant: true } });
|
const job = await prisma.exportJob.findUnique({ where: { id: jobId }, include: { tenant: true } });
|
||||||
@@ -47,7 +49,7 @@ const buildExport = async (jobId: string) => {
|
|||||||
|
|
||||||
await prisma.exportJob.update({
|
await prisma.exportJob.update({
|
||||||
where: { id: job.id },
|
where: { id: job.id },
|
||||||
data: { status: "DONE", filePath }
|
data: { status: "DONE", filePath, expiresAt: new Date(Date.now() + EXPORT_TTL_HOURS * 3600 * 1000) }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ export const processExportQueue = async () => {
|
|||||||
orderBy: { createdAt: "asc" }
|
orderBy: { createdAt: "asc" }
|
||||||
});
|
});
|
||||||
if (!job) {
|
if (!job) {
|
||||||
|
await cleanupExpiredExports();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { z } from "zod";
|
|||||||
import { prisma } from "../db.js";
|
import { prisma } from "../db.js";
|
||||||
import { logJobEvent } from "../queue/jobEvents.js";
|
import { logJobEvent } from "../queue/jobEvents.js";
|
||||||
import { queueCleanupJob, removeQueueJob } from "../queue/queue.js";
|
import { queueCleanupJob, removeQueueJob } from "../queue/queue.js";
|
||||||
|
import { createReadStream } from "node:fs";
|
||||||
|
import { access } from "node:fs/promises";
|
||||||
|
|
||||||
const roleSchema = z.object({
|
const roleSchema = z.object({
|
||||||
role: z.enum(["USER", "ADMIN"])
|
role: z.enum(["USER", "ADMIN"])
|
||||||
@@ -94,10 +96,34 @@ export async function adminRoutes(app: FastifyInstance) {
|
|||||||
const params = request.params as { id: string };
|
const params = request.params as { id: string };
|
||||||
const job = await prisma.exportJob.findUnique({ where: { id: params.id } });
|
const job = await prisma.exportJob.findUnique({ where: { id: params.id } });
|
||||||
if (!job || !job.filePath) return reply.code(404).send({ message: "Export file not ready" });
|
if (!job || !job.filePath) return reply.code(404).send({ message: "Export file not ready" });
|
||||||
|
if (job.expiresAt && job.expiresAt < new Date()) {
|
||||||
|
return reply.code(410).send({ message: "Export expired" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(job.filePath);
|
||||||
|
} catch {
|
||||||
|
return reply.code(404).send({ message: "Export file missing" });
|
||||||
|
}
|
||||||
|
|
||||||
reply.header("Content-Type", "application/zip");
|
reply.header("Content-Type", "application/zip");
|
||||||
reply.header("Content-Disposition", `attachment; filename=export-${job.id}.zip`);
|
reply.header("Content-Disposition", `attachment; filename=export-${job.id}.zip`);
|
||||||
return reply.sendFile(job.filePath);
|
return reply.send(createReadStream(job.filePath));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/exports", async () => {
|
||||||
|
const exports = await prisma.exportJob.findMany({
|
||||||
|
orderBy: { createdAt: "desc" }
|
||||||
|
});
|
||||||
|
const sanitized = exports.map((job) => ({
|
||||||
|
id: job.id,
|
||||||
|
status: job.status,
|
||||||
|
format: job.format,
|
||||||
|
scope: job.scope,
|
||||||
|
expiresAt: job.expiresAt,
|
||||||
|
createdAt: job.createdAt
|
||||||
|
}));
|
||||||
|
return { exports: sanitized };
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete("/tenants/:id", async (request, reply) => {
|
app.delete("/tenants/:id", async (request, reply) => {
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ export async function queueRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
app.get("/exports/:id/stream", async (request, reply) => {
|
app.get("/exports/:id/stream", async (request, reply) => {
|
||||||
const params = request.params as { id: string };
|
const params = request.params as { id: string };
|
||||||
|
const user = await prisma.user.findUnique({ where: { id: request.user.sub } });
|
||||||
|
if (!user || user.role !== "ADMIN") {
|
||||||
|
return reply.code(403).send({ message: "Forbidden" });
|
||||||
|
}
|
||||||
const exportJob = await prisma.exportJob.findUnique({ where: { id: params.id } });
|
const exportJob = await prisma.exportJob.findUnique({ where: { id: params.id } });
|
||||||
if (!exportJob) {
|
if (!exportJob) {
|
||||||
return reply.code(404).send({ message: "Export job not found" });
|
return reply.code(404).send({ message: "Export job not found" });
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ services:
|
|||||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
||||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
||||||
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost:8000/oauth/gmail/callback}
|
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost:8000/oauth/gmail/callback}
|
||||||
|
EXPORT_DIR: /tmp/mailcleaner-exports
|
||||||
|
EXPORT_TTL_HOURS: 24
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
@@ -50,6 +52,8 @@ services:
|
|||||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
||||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
||||||
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost:8000/oauth/gmail/callback}
|
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost:8000/oauth/gmail/callback}
|
||||||
|
EXPORT_DIR: /tmp/mailcleaner-exports
|
||||||
|
EXPORT_TTL_HOURS: 24
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { apiFetch, createEventSourceFor } from "./api";
|
import { apiFetch, createEventSourceFor } from "./api";
|
||||||
import { downloadFile } from "./export";
|
import { downloadFile } from "./export";
|
||||||
|
import { downloadExport } from "./exportHistory";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Tenant = {
|
type Tenant = {
|
||||||
@@ -51,6 +52,7 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
|||||||
const [exportTenantId, setExportTenantId] = useState<string | null>(null);
|
const [exportTenantId, setExportTenantId] = useState<string | null>(null);
|
||||||
const [exportStatus, setExportStatus] = useState<"idle" | "loading" | "done" | "failed">("idle");
|
const [exportStatus, setExportStatus] = useState<"idle" | "loading" | "done" | "failed">("idle");
|
||||||
const [exportJobId, setExportJobId] = useState<string | null>(null);
|
const [exportJobId, setExportJobId] = useState<string | null>(null);
|
||||||
|
const [exportHistory, setExportHistory] = useState<{ id: string; status: string; expiresAt?: string | null; createdAt?: string }[]>([]);
|
||||||
const [exportScope, setExportScope] = useState<"all" | "users" | "accounts" | "jobs" | "rules">("all");
|
const [exportScope, setExportScope] = useState<"all" | "users" | "accounts" | "jobs" | "rules">("all");
|
||||||
const [exportFormat, setExportFormat] = useState<"json" | "csv" | "zip">("json");
|
const [exportFormat, setExportFormat] = useState<"json" | "csv" | "zip">("json");
|
||||||
const [tenantSort, setTenantSort] = useState<"recent" | "oldest" | "name">("recent");
|
const [tenantSort, setTenantSort] = useState<"recent" | "oldest" | "name">("recent");
|
||||||
@@ -70,6 +72,8 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
|||||||
|
|
||||||
const jobsData = await apiFetch("/admin/jobs", {}, token);
|
const jobsData = await apiFetch("/admin/jobs", {}, token);
|
||||||
setJobs(jobsData.jobs ?? []);
|
setJobs(jobsData.jobs ?? []);
|
||||||
|
const exportsData = await apiFetch("/admin/exports", {}, token);
|
||||||
|
setExportHistory(exportsData.exports ?? []);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -98,14 +102,15 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
|||||||
} else {
|
} else {
|
||||||
const result = await apiFetch(`/admin/tenants/${tenant.id}/export?format=zip&scope=${exportScope}`, {}, token);
|
const result = await apiFetch(`/admin/tenants/${tenant.id}/export?format=zip&scope=${exportScope}`, {}, token);
|
||||||
setExportJobId(result.jobId);
|
setExportJobId(result.jobId);
|
||||||
|
setExportHistory((prev) => [{ id: result.jobId, status: "QUEUED" }, ...prev]);
|
||||||
const source = createEventSourceFor(`exports/${result.jobId}`, token);
|
const source = createEventSourceFor(`exports/${result.jobId}`, token);
|
||||||
source.onmessage = async (event) => {
|
source.onmessage = async (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
setExportHistory((prev) =>
|
||||||
|
prev.map((item) => (item.id === data.id ? { ...item, status: data.status, expiresAt: data.expiresAt } : item))
|
||||||
|
);
|
||||||
if (data.status === "DONE") {
|
if (data.status === "DONE") {
|
||||||
const base = import.meta.env.VITE_API_URL ?? "http://localhost:8000";
|
const response = await downloadExport(token, result.jobId);
|
||||||
const response = await fetch(`${base}/admin/exports/${result.jobId}/download`, {
|
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
downloadFile(blob, `tenant-${tenant.id}.zip`);
|
downloadFile(blob, `tenant-${tenant.id}.zip`);
|
||||||
setExportStatus("done");
|
setExportStatus("done");
|
||||||
@@ -309,6 +314,48 @@ export default function AdminPanel({ token, onImpersonate }: Props) {
|
|||||||
: ""}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{exportHistory.length > 0 && (
|
||||||
|
<div className="export-history">
|
||||||
|
<h4>{t("exportHistory")}</h4>
|
||||||
|
{exportHistory.map((item) => (
|
||||||
|
<div key={item.id} className="list-item">
|
||||||
|
<div>
|
||||||
|
<strong>{item.id.slice(0, 6)}</strong>
|
||||||
|
<p>
|
||||||
|
{item.expiresAt && new Date(item.expiresAt) < new Date()
|
||||||
|
? t("exportStatusExpired")
|
||||||
|
: item.status === "QUEUED"
|
||||||
|
? t("exportStatusQueued")
|
||||||
|
: item.status === "RUNNING"
|
||||||
|
? t("exportStatusRunning")
|
||||||
|
: item.status === "DONE"
|
||||||
|
? t("exportStatusDone")
|
||||||
|
: t("exportStatusFailed")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="inline-actions">
|
||||||
|
<span>
|
||||||
|
{item.createdAt ? new Date(item.createdAt).toLocaleString() : "-"} ·{" "}
|
||||||
|
{t("exportExpires")}: {item.expiresAt ? new Date(item.expiresAt).toLocaleString() : "-"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="ghost"
|
||||||
|
disabled={item.status !== "DONE" || (item.expiresAt ? new Date(item.expiresAt) < new Date() : false)}
|
||||||
|
onClick={async () => {
|
||||||
|
const response = await downloadExport(token, item.id);
|
||||||
|
if (response.ok) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
downloadFile(blob, `export-${item.id}.zip`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("exportDownload")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
13
frontend/src/exportHistory.ts
Normal file
13
frontend/src/exportHistory.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { apiFetch } from "./api";
|
||||||
|
|
||||||
|
export const downloadExport = async (token: string, exportId: string) => {
|
||||||
|
const base = import.meta.env.VITE_API_URL ?? "http://localhost:8000";
|
||||||
|
const response = await fetch(`${base}/admin/exports/${exportId}/download`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchExportJob = async (token: string, exportId: string) => {
|
||||||
|
return apiFetch(`/admin/exports/${exportId}`, {}, token);
|
||||||
|
};
|
||||||
@@ -143,5 +143,13 @@
|
|||||||
"adminSortOldest": "Älteste",
|
"adminSortOldest": "Älteste",
|
||||||
"adminSortName": "Name",
|
"adminSortName": "Name",
|
||||||
"adminSortEmail": "Email",
|
"adminSortEmail": "Email",
|
||||||
"adminSortStatus": "Status"
|
"adminSortStatus": "Status",
|
||||||
|
"exportHistory": "Export Historie",
|
||||||
|
"exportDownload": "Download",
|
||||||
|
"exportExpires": "Läuft ab",
|
||||||
|
"exportStatusQueued": "In Warteschlange",
|
||||||
|
"exportStatusRunning": "Läuft",
|
||||||
|
"exportStatusDone": "Fertig",
|
||||||
|
"exportStatusFailed": "Fehlgeschlagen",
|
||||||
|
"exportStatusExpired": "Abgelaufen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,5 +143,13 @@
|
|||||||
"adminSortOldest": "Oldest",
|
"adminSortOldest": "Oldest",
|
||||||
"adminSortName": "Name",
|
"adminSortName": "Name",
|
||||||
"adminSortEmail": "Email",
|
"adminSortEmail": "Email",
|
||||||
"adminSortStatus": "Status"
|
"adminSortStatus": "Status",
|
||||||
|
"exportHistory": "Export history",
|
||||||
|
"exportDownload": "Download",
|
||||||
|
"exportExpires": "Expires",
|
||||||
|
"exportStatusQueued": "Queued",
|
||||||
|
"exportStatusRunning": "Running",
|
||||||
|
"exportStatusDone": "Done",
|
||||||
|
"exportStatusFailed": "Failed",
|
||||||
|
"exportStatusExpired": "Expired"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,6 +274,11 @@ h1 {
|
|||||||
border-color: var(--secondary);
|
border-color: var(--secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user