rl-core-api 0.15.6 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/bootstrap/bootstrapCore.util.js +1 -1
- package/dist/core/config/env.schema.d.ts +7 -0
- package/dist/core/config/env.schema.js +4 -0
- package/dist/core/database/migrations/1789171200000-NotificationDismissal.d.ts +6 -0
- package/dist/core/database/migrations/1789171200000-NotificationDismissal.js +16 -0
- package/dist/core/database/migrations/index.js +2 -0
- package/dist/core/images/imageCleanup.service.d.ts +9 -0
- package/dist/core/images/imageCleanup.service.js +50 -0
- package/dist/core/images/imageFetch.service.d.ts +11 -0
- package/dist/core/images/imageFetch.service.js +190 -0
- package/dist/core/images/imageProcessing.service.d.ts +9 -0
- package/dist/core/images/imageProcessing.service.js +74 -0
- package/dist/core/images/imageProfile.interface.d.ts +40 -0
- package/dist/core/images/imageProfile.interface.js +29 -0
- package/dist/core/images/imageProfile.registry.d.ts +14 -0
- package/dist/core/images/imageProfile.registry.js +61 -0
- package/dist/core/images/imageStorage.service.d.ts +28 -0
- package/dist/core/images/imageStorage.service.js +151 -0
- package/dist/core/images/images.controller.d.ts +16 -0
- package/dist/core/images/images.controller.js +142 -0
- package/dist/core/images/images.module.d.ts +2 -0
- package/dist/core/images/images.module.js +35 -0
- package/dist/core/images/images.processor.d.ts +18 -0
- package/dist/core/images/images.processor.js +75 -0
- package/dist/core/images/images.service.d.ts +34 -0
- package/dist/core/images/images.service.js +137 -0
- package/dist/core/images/imagesApi.module.d.ts +2 -0
- package/dist/core/images/imagesApi.module.js +25 -0
- package/dist/core/images/imagesWorker.module.d.ts +2 -0
- package/dist/core/images/imagesWorker.module.js +22 -0
- package/dist/core/images/responses/imageAccepted.response.d.ts +4 -0
- package/dist/core/images/responses/imageAccepted.response.js +28 -0
- package/dist/core/images/responses/imageProfile.response.d.ts +10 -0
- package/dist/core/images/responses/imageProfile.response.js +55 -0
- package/dist/core/images/responses/imageStatus.response.d.ts +7 -0
- package/dist/core/images/responses/imageStatus.response.js +36 -0
- package/dist/core/images/uploadImageFromUrl.command.d.ts +3 -0
- package/dist/core/images/uploadImageFromUrl.command.js +23 -0
- package/dist/core/queue/job.producer.d.ts +1 -1
- package/dist/core/queue/job.producer.js +7 -1
- package/dist/core/queue/queue.constants.d.ts +4 -1
- package/dist/core/queue/queue.constants.js +5 -2
- package/dist/features/notifications/domain/notifications.service.d.ts +2 -0
- package/dist/features/notifications/domain/notifications.service.js +12 -0
- package/dist/features/notifications/infra/repositories/notification.repository.d.ts +1 -0
- package/dist/features/notifications/infra/repositories/notification.repository.js +8 -0
- package/dist/features/notifications/infra/repositories/notificationRead.repository.d.ts +1 -0
- package/dist/features/notifications/infra/repositories/notificationRead.repository.js +17 -0
- package/dist/features/notifications/infra/schema/notificationRead.schema.d.ts +1 -0
- package/dist/features/notifications/infra/schema/notificationRead.schema.js +4 -0
- package/dist/features/notifications/presentation/notifications.controller.d.ts +2 -0
- package/dist/features/notifications/presentation/notifications.controller.js +23 -0
- package/dist/features/users/domain/avatarImage.profile.d.ts +13 -0
- package/dist/features/users/domain/avatarImage.profile.js +30 -0
- package/dist/features/users/domain/index.d.ts +1 -1
- package/dist/features/users/domain/index.js +1 -1
- package/dist/features/users/presentation/users.controller.d.ts +6 -5
- package/dist/features/users/presentation/users.controller.js +28 -15
- package/dist/features/users/users.module.js +3 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +43 -2
- package/dist/rlCore.module.js +5 -1
- package/package.json +1 -1
- package/dist/features/users/domain/avatar.service.d.ts +0 -5
- package/dist/features/users/domain/avatar.service.js +0 -61
|
@@ -29,7 +29,7 @@ const bootstrapCore = async (appModule, options = {}) => {
|
|
|
29
29
|
if (options.bodyParser === "all") {
|
|
30
30
|
app.useBodyParser("urlencoded", { extended: true });
|
|
31
31
|
}
|
|
32
|
-
app.useStaticAssets((0, path_1.join)(process.cwd(), options.uploadsDir ?? "uploads"), {
|
|
32
|
+
app.useStaticAssets((0, path_1.join)(process.cwd(), options.uploadsDir ?? process.env.UPLOADS_DIR ?? "uploads"), {
|
|
33
33
|
prefix: "/uploads/",
|
|
34
34
|
setHeaders: (res) => {
|
|
35
35
|
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
@@ -69,6 +69,13 @@ export declare const envSchema: z.ZodObject<{
|
|
|
69
69
|
false: "false";
|
|
70
70
|
}>, z.ZodTransform<boolean, "true" | "false">>>;
|
|
71
71
|
EXPORT_MAX_ROWS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
72
|
+
UPLOADS_DIR: z.ZodDefault<z.ZodString>;
|
|
73
|
+
IMAGE_INCOMING_DIR: z.ZodDefault<z.ZodString>;
|
|
74
|
+
IMAGE_PENDING_TTL_HOURS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
75
|
+
IMAGE_CLEANUP_ENABLED: z.ZodDefault<z.ZodPipe<z.ZodEnum<{
|
|
76
|
+
true: "true";
|
|
77
|
+
false: "false";
|
|
78
|
+
}>, z.ZodTransform<boolean, "true" | "false">>>;
|
|
72
79
|
TOKEN_CLEANUP_CRON: z.ZodDefault<z.ZodString>;
|
|
73
80
|
AUDIT_SLOW_REQUEST_MS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
74
81
|
AUDIT_IP_SALT: z.ZodOptional<z.ZodString>;
|
|
@@ -85,6 +85,10 @@ exports.envSchema = zod_1.z
|
|
|
85
85
|
EXPORT_TTL_HOURS: zod_1.z.coerce.number().int().positive().default(3),
|
|
86
86
|
EXPORT_CLEANUP_ENABLED: booleanFromString.default(true),
|
|
87
87
|
EXPORT_MAX_ROWS: zod_1.z.coerce.number().int().positive().default(500_000),
|
|
88
|
+
UPLOADS_DIR: zod_1.z.string().min(1).default("uploads"),
|
|
89
|
+
IMAGE_INCOMING_DIR: zod_1.z.string().min(1).default("incoming"),
|
|
90
|
+
IMAGE_PENDING_TTL_HOURS: zod_1.z.coerce.number().int().positive().default(24),
|
|
91
|
+
IMAGE_CLEANUP_ENABLED: booleanFromString.default(true),
|
|
88
92
|
TOKEN_CLEANUP_CRON: zod_1.z.string().default("0 */6 * * *"),
|
|
89
93
|
AUDIT_SLOW_REQUEST_MS: zod_1.z.coerce.number().int().positive().default(1000),
|
|
90
94
|
AUDIT_IP_SALT: zod_1.z.string().optional(),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotificationDismissal1789171200000 = void 0;
|
|
4
|
+
class NotificationDismissal1789171200000 {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = "NotificationDismissal1789171200000";
|
|
7
|
+
}
|
|
8
|
+
async up(queryRunner) {
|
|
9
|
+
await queryRunner.query(`ALTER TABLE notification_reads
|
|
10
|
+
ADD COLUMN dismissed_at datetime NULL AFTER read_at`);
|
|
11
|
+
}
|
|
12
|
+
async down(queryRunner) {
|
|
13
|
+
await queryRunner.query(`ALTER TABLE notification_reads DROP COLUMN dismissed_at`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.NotificationDismissal1789171200000 = NotificationDismissal1789171200000;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.coreMigrations = void 0;
|
|
4
|
+
const _1789171200000_NotificationDismissal_1 = require("./1789171200000-NotificationDismissal");
|
|
4
5
|
const _1710000000000_InitialSchema_1 = require("./1710000000000-InitialSchema");
|
|
5
6
|
exports.coreMigrations = [
|
|
6
7
|
_1710000000000_InitialSchema_1.InitialSchema1710000000000,
|
|
8
|
+
_1789171200000_NotificationDismissal_1.NotificationDismissal1789171200000,
|
|
7
9
|
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { EnvService } from "../config/env.service";
|
|
2
|
+
import { ImageStorageService } from "./imageStorage.service";
|
|
3
|
+
export declare class ImageCleanupService {
|
|
4
|
+
private readonly storage;
|
|
5
|
+
private readonly env;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
constructor(storage: ImageStorageService, env: EnvService);
|
|
8
|
+
sweep(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var ImageCleanupService_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.ImageCleanupService = void 0;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const schedule_1 = require("@nestjs/schedule");
|
|
16
|
+
const env_service_1 = require("../config/env.service");
|
|
17
|
+
const imageStorage_service_1 = require("./imageStorage.service");
|
|
18
|
+
const HOUR_IN_MS = 60 * 60 * 1000;
|
|
19
|
+
const INCOMING_MAX_AGE_MS = HOUR_IN_MS;
|
|
20
|
+
let ImageCleanupService = ImageCleanupService_1 = class ImageCleanupService {
|
|
21
|
+
constructor(storage, env) {
|
|
22
|
+
this.storage = storage;
|
|
23
|
+
this.env = env;
|
|
24
|
+
this.logger = new common_1.Logger(ImageCleanupService_1.name);
|
|
25
|
+
}
|
|
26
|
+
async sweep() {
|
|
27
|
+
if (!this.env.get("IMAGE_CLEANUP_ENABLED")) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const pendingMaxAgeMs = this.env.get("IMAGE_PENDING_TTL_HOURS") * HOUR_IN_MS;
|
|
31
|
+
try {
|
|
32
|
+
await this.storage.removeExpired(pendingMaxAgeMs, INCOMING_MAX_AGE_MS);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
this.logger.error(`Falha na faxina de imagens: ${error.message}`, error.stack);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
exports.ImageCleanupService = ImageCleanupService;
|
|
40
|
+
__decorate([
|
|
41
|
+
(0, schedule_1.Cron)(schedule_1.CronExpression.EVERY_30_MINUTES),
|
|
42
|
+
__metadata("design:type", Function),
|
|
43
|
+
__metadata("design:paramtypes", []),
|
|
44
|
+
__metadata("design:returntype", Promise)
|
|
45
|
+
], ImageCleanupService.prototype, "sweep", null);
|
|
46
|
+
exports.ImageCleanupService = ImageCleanupService = ImageCleanupService_1 = __decorate([
|
|
47
|
+
(0, common_1.Injectable)(),
|
|
48
|
+
__metadata("design:paramtypes", [imageStorage_service_1.ImageStorageService,
|
|
49
|
+
env_service_1.EnvService])
|
|
50
|
+
], ImageCleanupService);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const isBlockedAddress: (address: string) => boolean;
|
|
2
|
+
export declare class ImageFetchService {
|
|
3
|
+
private readonly logger;
|
|
4
|
+
assertAllowed(rawUrl: string): Promise<void>;
|
|
5
|
+
download(rawUrl: string, maxBytes: number): Promise<Buffer>;
|
|
6
|
+
private parse;
|
|
7
|
+
private request;
|
|
8
|
+
private guardedLookup;
|
|
9
|
+
private tooLarge;
|
|
10
|
+
private wrap;
|
|
11
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var ImageFetchService_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ImageFetchService = exports.isBlockedAddress = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const dns_1 = require("dns");
|
|
13
|
+
const http_1 = require("http");
|
|
14
|
+
const https_1 = require("https");
|
|
15
|
+
const net_1 = require("net");
|
|
16
|
+
const TOTAL_TIMEOUT_MS = 15_000;
|
|
17
|
+
const SOCKET_TIMEOUT_MS = 8_000;
|
|
18
|
+
const MAX_REDIRECTS = 3;
|
|
19
|
+
const isBlockedIPv4 = (address) => {
|
|
20
|
+
const [a, b] = address.split(".").map(Number);
|
|
21
|
+
return (a === 0 ||
|
|
22
|
+
a === 10 ||
|
|
23
|
+
a === 127 ||
|
|
24
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
25
|
+
(a === 169 && b === 254) ||
|
|
26
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
27
|
+
(a === 192 && b === 168) ||
|
|
28
|
+
(a === 192 && b === 0) ||
|
|
29
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
30
|
+
a >= 224);
|
|
31
|
+
};
|
|
32
|
+
const isBlockedIPv6 = (address) => {
|
|
33
|
+
const value = address.toLowerCase().replace(/^\[|\]$/g, "");
|
|
34
|
+
const mapped = /^::ffff:(?<v4>\d+\.\d+\.\d+\.\d+)$/.exec(value);
|
|
35
|
+
if (mapped?.groups) {
|
|
36
|
+
return isBlockedIPv4(mapped.groups.v4);
|
|
37
|
+
}
|
|
38
|
+
return (value === "::" ||
|
|
39
|
+
value === "::1" ||
|
|
40
|
+
value.startsWith("fe8") ||
|
|
41
|
+
value.startsWith("fe9") ||
|
|
42
|
+
value.startsWith("fea") ||
|
|
43
|
+
value.startsWith("feb") ||
|
|
44
|
+
value.startsWith("fc") ||
|
|
45
|
+
value.startsWith("fd") ||
|
|
46
|
+
value.startsWith("ff"));
|
|
47
|
+
};
|
|
48
|
+
const isBlockedAddress = (address) => (0, net_1.isIP)(address) === 6 ? isBlockedIPv6(address) : isBlockedIPv4(address);
|
|
49
|
+
exports.isBlockedAddress = isBlockedAddress;
|
|
50
|
+
const lookupAll = (hostname) => new Promise((resolve, reject) => {
|
|
51
|
+
(0, dns_1.lookup)(hostname, { all: true }, (error, addresses) => {
|
|
52
|
+
if (error) {
|
|
53
|
+
reject(error);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
resolve(addresses);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
let ImageFetchService = ImageFetchService_1 = class ImageFetchService {
|
|
60
|
+
constructor() {
|
|
61
|
+
this.logger = new common_1.Logger(ImageFetchService_1.name);
|
|
62
|
+
this.guardedLookup = (hostname, options, callback) => {
|
|
63
|
+
(0, dns_1.lookup)(hostname, { all: true }, (error, addresses) => {
|
|
64
|
+
if (error) {
|
|
65
|
+
callback(error, "", undefined);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const blocked = addresses.find((entry) => (0, exports.isBlockedAddress)(entry.address));
|
|
69
|
+
if (blocked) {
|
|
70
|
+
this.logger.warn(`Endereço recusado por apontar para a rede interna: ${hostname} → ${blocked.address}`);
|
|
71
|
+
callback(Object.assign(new Error("endereço não permitido"), {
|
|
72
|
+
code: "EACCES",
|
|
73
|
+
}), "", undefined);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
callback(null, addresses, undefined);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async assertAllowed(rawUrl) {
|
|
81
|
+
const url = this.parse(rawUrl);
|
|
82
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
83
|
+
if ((0, net_1.isIP)(host)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const addresses = await lookupAll(host).catch(() => []);
|
|
87
|
+
const blocked = addresses.find((entry) => (0, exports.isBlockedAddress)(entry.address));
|
|
88
|
+
if (blocked) {
|
|
89
|
+
this.logger.warn(`Endereço recusado por apontar para a rede interna: ${host} → ${blocked.address}`);
|
|
90
|
+
throw new common_1.BadRequestException("Endereço não permitido");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async download(rawUrl, maxBytes) {
|
|
94
|
+
let url = this.parse(rawUrl);
|
|
95
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
|
|
96
|
+
const response = await this.request(url, maxBytes);
|
|
97
|
+
if (!response.location) {
|
|
98
|
+
return response.body;
|
|
99
|
+
}
|
|
100
|
+
response.discard();
|
|
101
|
+
url = this.parse(new URL(response.location, url).toString());
|
|
102
|
+
}
|
|
103
|
+
throw new common_1.BadRequestException("O endereço redireciona demais. Baixe a imagem e envie o arquivo");
|
|
104
|
+
}
|
|
105
|
+
parse(rawUrl) {
|
|
106
|
+
let url;
|
|
107
|
+
try {
|
|
108
|
+
url = new URL(rawUrl);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new common_1.BadRequestException("Endereço inválido");
|
|
112
|
+
}
|
|
113
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
114
|
+
throw new common_1.BadRequestException("Endereço precisa começar com http ou https");
|
|
115
|
+
}
|
|
116
|
+
const literal = url.hostname.replace(/^\[|\]$/g, "");
|
|
117
|
+
if ((0, net_1.isIP)(literal) && (0, exports.isBlockedAddress)(literal)) {
|
|
118
|
+
throw new common_1.BadRequestException("Endereço não permitido");
|
|
119
|
+
}
|
|
120
|
+
return url;
|
|
121
|
+
}
|
|
122
|
+
request(url, maxBytes) {
|
|
123
|
+
const get = url.protocol === "https:" ? https_1.get : http_1.get;
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const request = get(url, {
|
|
126
|
+
timeout: SOCKET_TIMEOUT_MS,
|
|
127
|
+
lookup: this.guardedLookup,
|
|
128
|
+
headers: {
|
|
129
|
+
accept: "image/*",
|
|
130
|
+
},
|
|
131
|
+
}, (response) => {
|
|
132
|
+
const status = response.statusCode ?? 0;
|
|
133
|
+
if (status >= 300 && status < 400 && response.headers.location) {
|
|
134
|
+
resolve({
|
|
135
|
+
body: Buffer.alloc(0),
|
|
136
|
+
location: response.headers.location,
|
|
137
|
+
discard: () => response.resume(),
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (status !== 200) {
|
|
142
|
+
response.resume();
|
|
143
|
+
reject(new common_1.BadRequestException(`O endereço respondeu ${status || "sem status"}. Baixe a imagem e envie o arquivo`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const declared = Number(response.headers["content-length"] ?? 0);
|
|
147
|
+
if (declared > maxBytes) {
|
|
148
|
+
response.destroy();
|
|
149
|
+
reject(this.tooLarge(maxBytes));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const chunks = [];
|
|
153
|
+
let received = 0;
|
|
154
|
+
response.on("data", (chunk) => {
|
|
155
|
+
received += chunk.length;
|
|
156
|
+
if (received > maxBytes) {
|
|
157
|
+
response.destroy();
|
|
158
|
+
reject(this.tooLarge(maxBytes));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
chunks.push(chunk);
|
|
162
|
+
});
|
|
163
|
+
response.on("end", () => resolve({ body: Buffer.concat(chunks), discard: () => undefined }));
|
|
164
|
+
response.on("error", (error) => reject(this.wrap(error)));
|
|
165
|
+
});
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
request.destroy(new Error("tempo esgotado"));
|
|
168
|
+
}, TOTAL_TIMEOUT_MS);
|
|
169
|
+
timer.unref();
|
|
170
|
+
request.on("timeout", () => request.destroy(new Error("tempo esgotado")));
|
|
171
|
+
request.on("error", (error) => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
reject(this.wrap(error));
|
|
174
|
+
});
|
|
175
|
+
request.on("close", () => clearTimeout(timer));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
tooLarge(maxBytes) {
|
|
179
|
+
const mb = Math.round(maxBytes / (1024 * 1024));
|
|
180
|
+
return new common_1.BadRequestException(`A imagem passa do limite de ${mb}MB`);
|
|
181
|
+
}
|
|
182
|
+
wrap(error) {
|
|
183
|
+
this.logger.warn(`Falha ao baixar imagem: ${error.message}`);
|
|
184
|
+
return new common_1.BadRequestException("Não foi possível baixar a imagem deste endereço");
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
exports.ImageFetchService = ImageFetchService;
|
|
188
|
+
exports.ImageFetchService = ImageFetchService = ImageFetchService_1 = __decorate([
|
|
189
|
+
(0, common_1.Injectable)()
|
|
190
|
+
], ImageFetchService);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ImageJobSummary, ImageProfile } from "./imageProfile.interface";
|
|
2
|
+
import { ImageStorageService } from "./imageStorage.service";
|
|
3
|
+
export declare class ImageProcessingService {
|
|
4
|
+
private readonly storage;
|
|
5
|
+
constructor(storage: ImageStorageService);
|
|
6
|
+
assertSupported(buffer: Buffer, declaredType?: string): void;
|
|
7
|
+
detectType(buffer: Buffer): string | null;
|
|
8
|
+
process(profile: ImageProfile, input: Buffer): Promise<ImageJobSummary>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ImageProcessingService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
18
|
+
const imageStorage_service_1 = require("./imageStorage.service");
|
|
19
|
+
const ALLOWED_INPUT = ["image/jpeg", "image/png", "image/webp"];
|
|
20
|
+
const MAGIC_BYTES = {
|
|
21
|
+
"image/jpeg": (buffer) => buffer.length >= 3 &&
|
|
22
|
+
buffer[0] === 0xff &&
|
|
23
|
+
buffer[1] === 0xd8 &&
|
|
24
|
+
buffer[2] === 0xff,
|
|
25
|
+
"image/png": (buffer) => buffer.length >= 8 &&
|
|
26
|
+
buffer
|
|
27
|
+
.subarray(0, 8)
|
|
28
|
+
.equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
|
|
29
|
+
"image/webp": (buffer) => buffer.length >= 12 &&
|
|
30
|
+
buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
|
|
31
|
+
buffer.subarray(8, 12).toString("ascii") === "WEBP",
|
|
32
|
+
};
|
|
33
|
+
const TRANSPARENT = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
34
|
+
let ImageProcessingService = class ImageProcessingService {
|
|
35
|
+
constructor(storage) {
|
|
36
|
+
this.storage = storage;
|
|
37
|
+
}
|
|
38
|
+
assertSupported(buffer, declaredType) {
|
|
39
|
+
const type = ALLOWED_INPUT.find((allowed) => allowed === declaredType);
|
|
40
|
+
if (!type) {
|
|
41
|
+
throw new common_1.BadRequestException("Formato inválido (use JPEG, PNG ou WebP)");
|
|
42
|
+
}
|
|
43
|
+
if (!MAGIC_BYTES[type](buffer)) {
|
|
44
|
+
throw new common_1.BadRequestException("O conteúdo do arquivo não corresponde ao formato declarado");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
detectType(buffer) {
|
|
48
|
+
return (ALLOWED_INPUT.find((type) => MAGIC_BYTES[type](buffer)) ?? null);
|
|
49
|
+
}
|
|
50
|
+
async process(profile, input) {
|
|
51
|
+
const pipeline = (0, sharp_1.default)(input, { failOn: "none" })
|
|
52
|
+
.rotate()
|
|
53
|
+
.resize({
|
|
54
|
+
width: profile.width,
|
|
55
|
+
height: profile.height,
|
|
56
|
+
fit: profile.fit,
|
|
57
|
+
withoutEnlargement: true,
|
|
58
|
+
background: TRANSPARENT,
|
|
59
|
+
});
|
|
60
|
+
const { data, info } = await pipeline
|
|
61
|
+
.toFormat(profile.format, { quality: profile.quality })
|
|
62
|
+
.toBuffer({ resolveWithObject: true })
|
|
63
|
+
.catch(() => {
|
|
64
|
+
throw new common_1.BadRequestException("Não foi possível processar esta imagem. Tente outro arquivo");
|
|
65
|
+
});
|
|
66
|
+
const path = await this.storage.savePending(profile, data);
|
|
67
|
+
return { path, width: info.width, height: info.height, bytes: info.size };
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
exports.ImageProcessingService = ImageProcessingService;
|
|
71
|
+
exports.ImageProcessingService = ImageProcessingService = __decorate([
|
|
72
|
+
(0, common_1.Injectable)(),
|
|
73
|
+
__metadata("design:paramtypes", [imageStorage_service_1.ImageStorageService])
|
|
74
|
+
], ImageProcessingService);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare enum ImageFit {
|
|
2
|
+
CONTAIN = "contain",
|
|
3
|
+
COVER = "cover",
|
|
4
|
+
INSIDE = "inside"
|
|
5
|
+
}
|
|
6
|
+
export declare enum ImageFormat {
|
|
7
|
+
WEBP = "webp",
|
|
8
|
+
JPEG = "jpeg",
|
|
9
|
+
PNG = "png"
|
|
10
|
+
}
|
|
11
|
+
export declare const EXTENSION_BY_FORMAT: Record<ImageFormat, string>;
|
|
12
|
+
export interface ImageProfile {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly folder: string;
|
|
15
|
+
readonly width: number;
|
|
16
|
+
readonly height?: number;
|
|
17
|
+
readonly fit: ImageFit;
|
|
18
|
+
readonly format: ImageFormat;
|
|
19
|
+
readonly quality: number;
|
|
20
|
+
readonly maxBytes: number;
|
|
21
|
+
readonly permission?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const IMAGE_PROFILE = "rl-core:image-profile";
|
|
24
|
+
export declare const ImageUploadProfile: () => ClassDecorator;
|
|
25
|
+
export declare enum ImageSourceKind {
|
|
26
|
+
FILE = "file",
|
|
27
|
+
URL = "url"
|
|
28
|
+
}
|
|
29
|
+
export interface ImageJobPayload {
|
|
30
|
+
profile: string;
|
|
31
|
+
kind: ImageSourceKind;
|
|
32
|
+
source: string;
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ImageJobSummary {
|
|
36
|
+
path: string;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
bytes: number;
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ImageSourceKind = exports.ImageUploadProfile = exports.IMAGE_PROFILE = exports.EXTENSION_BY_FORMAT = exports.ImageFormat = exports.ImageFit = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
var ImageFit;
|
|
6
|
+
(function (ImageFit) {
|
|
7
|
+
ImageFit["CONTAIN"] = "contain";
|
|
8
|
+
ImageFit["COVER"] = "cover";
|
|
9
|
+
ImageFit["INSIDE"] = "inside";
|
|
10
|
+
})(ImageFit || (exports.ImageFit = ImageFit = {}));
|
|
11
|
+
var ImageFormat;
|
|
12
|
+
(function (ImageFormat) {
|
|
13
|
+
ImageFormat["WEBP"] = "webp";
|
|
14
|
+
ImageFormat["JPEG"] = "jpeg";
|
|
15
|
+
ImageFormat["PNG"] = "png";
|
|
16
|
+
})(ImageFormat || (exports.ImageFormat = ImageFormat = {}));
|
|
17
|
+
exports.EXTENSION_BY_FORMAT = {
|
|
18
|
+
[ImageFormat.WEBP]: ".webp",
|
|
19
|
+
[ImageFormat.JPEG]: ".jpg",
|
|
20
|
+
[ImageFormat.PNG]: ".png",
|
|
21
|
+
};
|
|
22
|
+
exports.IMAGE_PROFILE = "rl-core:image-profile";
|
|
23
|
+
const ImageUploadProfile = () => (0, common_1.SetMetadata)(exports.IMAGE_PROFILE, true);
|
|
24
|
+
exports.ImageUploadProfile = ImageUploadProfile;
|
|
25
|
+
var ImageSourceKind;
|
|
26
|
+
(function (ImageSourceKind) {
|
|
27
|
+
ImageSourceKind["FILE"] = "file";
|
|
28
|
+
ImageSourceKind["URL"] = "url";
|
|
29
|
+
})(ImageSourceKind || (exports.ImageSourceKind = ImageSourceKind = {}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OnModuleInit } from "@nestjs/common";
|
|
2
|
+
import { DiscoveryService } from "@nestjs/core";
|
|
3
|
+
import { ImageProfile } from "./imageProfile.interface";
|
|
4
|
+
export declare class ImageProfileRegistry implements OnModuleInit {
|
|
5
|
+
private readonly discovery;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
private readonly profiles;
|
|
8
|
+
constructor(discovery: DiscoveryService);
|
|
9
|
+
onModuleInit(): void;
|
|
10
|
+
find(name: string): ImageProfile | undefined;
|
|
11
|
+
require(name: string): ImageProfile;
|
|
12
|
+
all(): ImageProfile[];
|
|
13
|
+
names(): string[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var ImageProfileRegistry_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.ImageProfileRegistry = void 0;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const core_1 = require("@nestjs/core");
|
|
16
|
+
const imageProfile_interface_1 = require("./imageProfile.interface");
|
|
17
|
+
let ImageProfileRegistry = ImageProfileRegistry_1 = class ImageProfileRegistry {
|
|
18
|
+
constructor(discovery) {
|
|
19
|
+
this.discovery = discovery;
|
|
20
|
+
this.logger = new common_1.Logger(ImageProfileRegistry_1.name);
|
|
21
|
+
this.profiles = new Map();
|
|
22
|
+
}
|
|
23
|
+
onModuleInit() {
|
|
24
|
+
const found = this.discovery
|
|
25
|
+
.getProviders()
|
|
26
|
+
.filter((wrapper) => wrapper.metatype &&
|
|
27
|
+
Reflect.getMetadata(imageProfile_interface_1.IMAGE_PROFILE, wrapper.metatype) === true)
|
|
28
|
+
.map((wrapper) => wrapper.instance)
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
for (const profile of found) {
|
|
31
|
+
if (this.profiles.has(profile.name)) {
|
|
32
|
+
throw new Error(`Dois perfis de imagem registrados com o nome "${profile.name}"`);
|
|
33
|
+
}
|
|
34
|
+
this.profiles.set(profile.name, profile);
|
|
35
|
+
}
|
|
36
|
+
if (this.profiles.size > 0) {
|
|
37
|
+
this.logger.log(`Perfis de imagem disponíveis: ${this.names().join(", ")}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
find(name) {
|
|
41
|
+
return this.profiles.get(name);
|
|
42
|
+
}
|
|
43
|
+
require(name) {
|
|
44
|
+
const profile = this.find(name);
|
|
45
|
+
if (!profile) {
|
|
46
|
+
throw new common_1.NotFoundException("Perfil de imagem não encontrado");
|
|
47
|
+
}
|
|
48
|
+
return profile;
|
|
49
|
+
}
|
|
50
|
+
all() {
|
|
51
|
+
return [...this.profiles.values()];
|
|
52
|
+
}
|
|
53
|
+
names() {
|
|
54
|
+
return [...this.profiles.keys()];
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
exports.ImageProfileRegistry = ImageProfileRegistry;
|
|
58
|
+
exports.ImageProfileRegistry = ImageProfileRegistry = ImageProfileRegistry_1 = __decorate([
|
|
59
|
+
(0, common_1.Injectable)(),
|
|
60
|
+
__metadata("design:paramtypes", [core_1.DiscoveryService])
|
|
61
|
+
], ImageProfileRegistry);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EnvService } from "../config/env.service";
|
|
2
|
+
import { ImageProfile } from "./imageProfile.interface";
|
|
3
|
+
export interface StoredImageRef {
|
|
4
|
+
folder: string;
|
|
5
|
+
id: string;
|
|
6
|
+
extension: string;
|
|
7
|
+
pending: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class ImageStorageService {
|
|
10
|
+
private readonly env;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
private readonly uploadsRoot;
|
|
13
|
+
private readonly incomingRoot;
|
|
14
|
+
constructor(env: EnvService);
|
|
15
|
+
saveIncoming(buffer: Buffer): Promise<string>;
|
|
16
|
+
readIncoming(id: string): Promise<Buffer>;
|
|
17
|
+
removeIncoming(id: string): Promise<void>;
|
|
18
|
+
savePending(profile: ImageProfile, data: Buffer): Promise<string>;
|
|
19
|
+
promote(publicPath: string | null | undefined): Promise<string | null>;
|
|
20
|
+
remove(publicPath: string | null | undefined): Promise<void>;
|
|
21
|
+
parsePublicPath(publicPath: string | null | undefined): StoredImageRef | null;
|
|
22
|
+
removeExpired(pendingMaxAgeMs: number, incomingMaxAgeMs: number): Promise<number>;
|
|
23
|
+
private pathFor;
|
|
24
|
+
private incomingPathFor;
|
|
25
|
+
private publicPathOf;
|
|
26
|
+
private absolute;
|
|
27
|
+
private sweep;
|
|
28
|
+
}
|