Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@eslint/js": "^10.0.1",
"@types/express": "^5.0.6",
"@types/node": "^25.5.0",
"@types/nodemailer": "^8.0.0",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"eslint": "^10.0.3",
Expand All @@ -40,7 +41,9 @@
"express": "^5.2.1",
"http-status-codes": "^2.3.0",
"mongoose": "^9.4.1",
"nodemailer": "^8.0.10",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1"
"swagger-ui-express": "^5.0.1",
"zod": "^4.4.3"
}
}
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { errorHandler } from './middleware/error.middleware';
import swaggerUi from 'swagger-ui-express';
import { swaggerSpec } from './utils/swagger';
import metricsRoutes from './routes/metrics.routes';
import emailRoutes from './routes/email.routes';

const app: Application = express();

Expand All @@ -24,6 +25,7 @@ app.use('/api/login', loginRoutes);
app.use('/api/appointment', appointmentRoutes);
app.use('/api/tips', tipsRoutes);
app.use('/api/metrics/', metricsRoutes);
app.use('/api/email', emailRoutes);

app.use(errorHandler);

Expand Down
56 changes: 56 additions & 0 deletions src/constants/email/email.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export const EMAIL_TYPE = {
ATTENDANCE_UPDATE: 'ATTENDANCE_UPDATE',
} as const;

export const ATTENDANCE_STATUS = {
COMING: 'COMING',
NOT_COMING: 'NOT_COMING',
} as const;

export const EMAIL_SUBJECTS = {
ATTENDANCE_COMING: 'אישור הגעה לטיפול',
ATTENDANCE_NOT_COMING: 'ביטול הגעה לטיפול',
} as const;

export const EMAIL_TEXT = {
dearCaregiver: 'מטפל יקר',
patientNumber: 'מספר מטופל',
willCome: 'יגיע לטיפול',
willNotCome: 'לא יוכל להגיע לטיפול',
treatmentTime: 'בשעה',
cell: 'תא',
building: 'בניין',
rescheduleMessage:
'אנא פנה למטופל לקביעת מועד חדש ועדכן במערכת כי התפנה כיסא.',
Comment on lines +23 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run Prettier on this file before merge.

These spots already trip prettier/prettier, so lint will stay red until the wrapped string and trailing newline are normalized.

Also applies to: 56-56

🧰 Tools
🪛 ESLint

[error] 23-24: Delete ⏎···

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/constants/email/email.constants.ts` around lines 23 - 24, The file has
formatting issues for the exported constant rescheduleMessage (and another at
the same file around line 56); run Prettier or reformat the string literals so
they are wrapped consistently and ensure the file ends with a single trailing
newline, then re-run lint/format checks; specifically update the
rescheduleMessage value in email.constants.ts to match project Prettier rules
(line-wrapping/quotes) and add the missing final newline.

automaticMessage: 'הודעה זו נשלחה אוטומטית ממערכת Sagol360.',
patientNameLabel: 'שם מטופל',
patientNumberLabel: 'מספר מטופל',
attendanceStatusLabel: 'סטטוס הגעה',
timeLabel: 'שעה',
cellLabel: 'תא',
buildingLabel: 'בניין',
comingStatusLabel: 'מגיע לטיפול',
notComingStatusLabel: 'לא מגיע לטיפול',
} as const;

export const EMAIL_ERRORS = {
invalidEmailData: 'Invalid email data',
unsupportedEmailType: 'Unsupported email type',
missingEmailEnvironmentVariables: 'Missing email environment variables',
emailWasNotSent: 'Email was not sent',
} as const;

export const EMAIL_SUCCESS_MESSAGES = {
emailSentSuccessfully: 'Email sent successfully',
} as const;

export const EMAIL_COLORS = {
comingHeader: '#1f7a4d',
notComingHeader: '#9f2f2f',
background: '#f4f6f8',
cardBackground: '#ffffff',
border: '#e0e0e0',
text: '#222222',
mutedText: '#777777',
tableLabelBackground: '#f8f9fa',
} as const;
26 changes: 26 additions & 0 deletions src/controllers/email/email.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Request, Response } from 'express';
import { StatusCodes } from 'http-status-codes';
import { sendEmail as serviceSendEmail } from '@/services/email/email.service';
import { EMAIL_ERRORS, EMAIL_SUCCESS_MESSAGES } from '@/constants/email/email.constants';

export const sendEmail = async (req: Request, res: Response) => {
try {
await serviceSendEmail(req.body);

res.status(StatusCodes.OK).json({
message: EMAIL_SUCCESS_MESSAGES.emailSentSuccessfully,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : EMAIL_ERRORS.emailWasNotSent;

const statusCode =
errorMessage === EMAIL_ERRORS.invalidEmailData ||
errorMessage === EMAIL_ERRORS.unsupportedEmailType
? StatusCodes.BAD_REQUEST
: StatusCodes.INTERNAL_SERVER_ERROR;

res.status(statusCode).json({
message: errorMessage,
});
}
};
99 changes: 99 additions & 0 deletions src/routes/email.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { sendEmail } from '@/controllers/email/email.controller';
import { Router } from 'express';

const router = Router();

/**
* @swagger
* /api/email:
* post:
* summary: Send an email
* description: Sends an attendance update email according to the provided email type and payload.
* tags:
* - Email
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - emailType
* - email
* - payload
* properties:
* emailType:
* type: string
* enum:
* - ATTENDANCE_UPDATE
* example: ATTENDANCE_UPDATE
* email:
* type: string
* format: email
* example: secretary@example.com
* payload:
* type: object
* required:
* - patientName
* - patientNumber
* - attendanceStatus
* - time
* - cell
* - building
* properties:
* patientName:
* type: string
* example: ישראל ישראלי
* patientNumber:
* type: string
* example: "123456"
* attendanceStatus:
* type: string
* enum:
* - COMING
* - NOT_COMING
* example: NOT_COMING
* time:
* type: string
* example: "18:00"
* cell:
* type: string
* example: כתום
* building:
* type: string
* example: אריסון
* responses:
* 200:
* description: Email sent successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Email sent successfully
* 400:
* description: Invalid email data or unsupported email type
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Invalid email data
* 500:
* description: Email was not sent or server email configuration is missing
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Email was not sent
*/
router.post('/', sendEmail);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Protect this endpoint before exposing it publicly.

src/app.ts mounts emailRoutes at /api/email after only express.json() and logger, and this route accepts arbitrary recipient addresses. Right now anyone who can reach the API can use your Gmail account as a mail relay. Add the same auth/authorization middleware used for staff-only actions here, and update the Swagger docs with 401/403 once it is in place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/email.routes.ts` at line 97, The POST /email route
(router.post('/', sendEmail)) is unprotected; wrap this route with the same
authentication and staff-authorization middleware used for staff-only actions
(apply the existing authenticate and authorizeStaff middleware functions in the
same order as other protected routes) so only authorized staff can send mail,
and ensure the route handler still receives the request after middleware. Also
update the Swagger/OpenAPI docs for this endpoint to include 401 and 403
responses and any required security schemes so clients know
authentication/authorization is required.


export default router;
18 changes: 18 additions & 0 deletions src/schemas/email/attendance-email.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ATTENDANCE_STATUS, EMAIL_TYPE } from '@/constants/email/email.constants';
import { sendEmailSchema } from '@/services/email/email.service';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether TS config preserves non-type imports and confirm dependency cycle edges.
fd -i 'tsconfig*.json' .
rg -n --no-heading -e 'verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues' $(fd -i 'tsconfig*.json' .)

# Confirm cross-imports between schema and service files.
rg -n --no-heading -e "from '`@/services/email/email.service`'" -e "from '`@/schemas/email/attendance-email.schema`'" src/services/email/email.service.ts src/schemas/email/attendance-email.schema.ts

Repository: ColmanDevClubORG/Sagol360ManagementServer

Length of output: 333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== tsconfig.json (relevant compilerOptions) ==="
cat -n tsconfig.json | sed -n '1,220p' | rg -n "compilerOptions|verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues|module|target|moduleResolution|esModuleInterop|isolatedModules|useDefineForClassFields" || true

echo
echo "=== attendance-email.schema.ts (imports + SendEmailData) ==="
nl -ba src/schemas/email/attendance-email.schema.ts | sed -n '1,120p'

echo
echo "=== email.service.ts (imports + attendanceEmailSchema usage) ==="
nl -ba src/services/email/email.service.ts | sed -n '1,160p'

Repository: ColmanDevClubORG/Sagol360ManagementServer

Length of output: 440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== tsconfig.json (compilerOptions excerpt) ==="
# Print the compilerOptions block only (small)
python3 - <<'PY'
import json
p='tsconfig.json'
with open(p,'r',encoding='utf-8') as f: data=json.load(f)
co=data.get('compilerOptions',{})
keys=[
'verbatimModuleSyntax',
'preserveValueImports',
'importHelpers',
'importsNotUsedAsValues',
'isolatedModules',
'module',
'moduleResolution',
'target',
'esModuleInterop',
'useUnknownInCatchVariables',
'strict',
'requireEmitForEachFunction',
'importsNotUsedAsValues'
]
print(json.dumps({k:co.get(k) for k in keys if k in co or k in ['module','moduleResolution','target','esModuleInterop']},indent=2))
PY

echo
echo "=== attendance-email.schema.ts (first 80 lines) ==="
cat -n src/schemas/email/attendance-email.schema.ts | sed -n '1,120p'

echo
echo "=== email.service.ts (first 140 lines) ==="
cat -n src/services/email/email.service.ts | sed -n '1,160p'

Repository: ColmanDevClubORG/Sagol360ManagementServer

Length of output: 3089


Remove the schema↔service import coupling by deriving SendEmailData from attendanceEmailSchema

attendance-email.schema.ts imports sendEmailSchema only for SendEmailData typing, while email.service.ts uses sendEmailSchema at runtime and imports attendanceEmailSchema back—creating a circular dependency. With the current tsconfig (NodeNext; no verbatimModuleSyntax/preserveValueImports/importsNotUsedAsValues overrides shown), this is likely not a runtime-cycle issue, but it’s still unnecessary coupling; derive the type directly from attendanceEmailSchema.

Suggested change
 import { ATTENDANCE_STATUS, EMAIL_TYPE } from '`@/constants/email/email.constants`';
-import { sendEmailSchema } from '`@/services/email/email.service`';
 import { z } from 'zod';
@@
-export type SendEmailData = z.infer<typeof sendEmailSchema>;
+export type SendEmailData = z.infer<typeof attendanceEmailSchema>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/schemas/email/attendance-email.schema.ts` at line 2,
attendance-email.schema.ts currently imports sendEmailSchema solely to reuse the
SendEmailData type, creating unnecessary coupling with email.service; instead
derive SendEmailData from attendanceEmailSchema itself. Remove the import of
sendEmailSchema, export or declare a type like SendEmailData = z.infer<typeof
attendanceEmailSchema> (reference attendanceEmailSchema and SendEmailData) so
email.service.ts can import the type from attendance-email.schema.ts without
creating a schema↔service import cycle; update any usages that referenced the
old sendEmailSchema type to use the new derived SendEmailData.

import { z } from 'zod';

export const attendanceEmailSchema = z.object({
emailType: z.literal(EMAIL_TYPE.ATTENDANCE_UPDATE),
email: z.string().trim().pipe(z.email()),
payload: z.object({
patientName: z.string().trim().min(1),
patientNumber: z.string().trim().min(1),
attendanceStatus: z.enum([ATTENDANCE_STATUS.COMING, ATTENDANCE_STATUS.NOT_COMING]),
time: z.string().trim().min(1),
cell: z.string().trim().min(1),
building: z.string().trim().min(1),
}),
});

export type SendEmailData = z.infer<typeof sendEmailSchema>;
13 changes: 13 additions & 0 deletions src/schemas/email/email.service.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { buildAttendanceEmailContent, EmailContent } from '@/services/email/email.service.helpers';
import { SendEmailData } from './attendance-email.schema';
import { EMAIL_ERRORS, EMAIL_TYPE } from '@/constants/email/email.constants';

export const buildEmailContent = (data: SendEmailData): EmailContent => {
switch (data.emailType) {
case EMAIL_TYPE.ATTENDANCE_UPDATE:
return buildAttendanceEmailContent(data.payload);

default:
throw new Error(EMAIL_ERRORS.unsupportedEmailType);
}
};
Loading