headers.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import type { Response } from 'express';
  2. import type { ExpressHttpHeader, IContentHeaders } from '~/server/interfaces/attachment';
  3. import type { IAttachmentDocument } from '~/server/models/attachment';
  4. import type { ConfigManager } from '~/server/service/config-manager';
  5. import type { ConfigKey } from '../../config-manager/config-definition';
  6. import { DEFAULT_ALLOWLIST_MIME_TYPES, SAFE_INLINE_CONFIGURABLE_MIME_TYPES } from './security';
  7. export class ContentHeaders implements IContentHeaders {
  8. contentType?: ExpressHttpHeader<'Content-Type'>;
  9. contentLength?: ExpressHttpHeader<'Content-Length'>;
  10. contentSecurityPolicy?: ExpressHttpHeader<'Content-Security-Policy'>;
  11. contentDisposition?: ExpressHttpHeader<'Content-Disposition'>;
  12. xContentTypeOptions?: ExpressHttpHeader<'X-Content-Type-Options'>;
  13. private configManager: ConfigManager; // Now explicitly typed and assigned in constructor
  14. // Refactored: Make constructor private and have it accept configManager
  15. private constructor(configManager: ConfigManager) {
  16. this.configManager = configManager;
  17. }
  18. // --- Factory Method (remains async) ---
  19. static async create(
  20. configManager: ConfigManager, // This parameter is now passed to the private constructor
  21. attachment: IAttachmentDocument,
  22. opts?: {
  23. inline?: boolean,
  24. },
  25. ): Promise<ContentHeaders> {
  26. // Create instance, passing the configManager to the private constructor
  27. const instance = new ContentHeaders(configManager);
  28. const attachmentContentType = attachment.fileFormat;
  29. const filename = attachment.originalName;
  30. const actualContentTypeString: string = attachmentContentType || 'application/octet-stream';
  31. instance.contentType = {
  32. field: 'Content-Type',
  33. value: actualContentTypeString,
  34. };
  35. const requestedInline = opts?.inline ?? false;
  36. const configKey = `attachments:contentDisposition:${actualContentTypeString}:inline` as ConfigKey;
  37. // AWAIT the config value here
  38. const rawConfigValue = await instance.configManager.getConfig(configKey); // Use instance's configManager
  39. let isConfiguredInline: boolean;
  40. if (typeof rawConfigValue === 'boolean') {
  41. isConfiguredInline = rawConfigValue;
  42. }
  43. else {
  44. isConfiguredInline = DEFAULT_ALLOWLIST_MIME_TYPES.has(actualContentTypeString);
  45. }
  46. const shouldBeInline = requestedInline
  47. && isConfiguredInline
  48. && SAFE_INLINE_CONFIGURABLE_MIME_TYPES.has(actualContentTypeString);
  49. console.log(shouldBeInline);
  50. console.log(`Should be inline for ${attachmentContentType}: ${shouldBeInline}`); // Enhanced log
  51. instance.contentDisposition = {
  52. field: 'Content-Disposition',
  53. value: shouldBeInline
  54. ? 'inline'
  55. : `attachment;filename*=UTF-8''${encodeURIComponent(filename)}`,
  56. };
  57. instance.contentSecurityPolicy = {
  58. field: 'Content-Security-Policy',
  59. value: "script-src 'unsafe-hashes'; style-src 'self' 'unsafe-inline'; object-src 'none'; require-trusted-types-for 'script'; media-src 'self'; default-src 'none';",
  60. };
  61. instance.xContentTypeOptions = {
  62. field: 'X-Content-Type-Options',
  63. value: 'nosniff',
  64. };
  65. if (attachment.fileSize) {
  66. instance.contentLength = {
  67. field: 'Content-Length',
  68. value: attachment.fileSize.toString(),
  69. };
  70. }
  71. return instance;
  72. }
  73. /**
  74. * Convert to ExpressHttpHeader[]
  75. */
  76. toExpressHttpHeaders(): ExpressHttpHeader[] {
  77. return [
  78. this.contentType,
  79. this.contentLength,
  80. this.contentSecurityPolicy,
  81. this.contentDisposition,
  82. this.xContentTypeOptions,
  83. ]
  84. // exclude undefined
  85. .filter((member): member is NonNullable<typeof member> => member != null);
  86. }
  87. }
  88. /**
  89. * Convert Record to ExpressHttpHeader[]
  90. */
  91. export const toExpressHttpHeaders = (records: Record<string, string | string[]>): ExpressHttpHeader[] => {
  92. return Object.entries(records).map(([field, value]) => { return { field, value } });
  93. };
  94. export const applyHeaders = (res: Response, headers: ExpressHttpHeader[]): void => {
  95. console.log('Applying headers:', headers);
  96. headers.forEach((header) => {
  97. res.header(header.field, header.value);
  98. });
  99. };