Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a1583eb
fix(auth-google): add log to callback
HRulier Aug 20, 2025
40c221c
fix(auth-google): add log to callback
HRulier Aug 20, 2025
c601514
fix(auth-google): add log to callback
HRulier Aug 20, 2025
03d3b3a
fix(auth-google): fix issue in findOrCreateUser
HRulier Aug 20, 2025
cdcd122
fix(auth-google): add log to callback
HRulier Aug 20, 2025
b8187d3
feat(rate-limiter): set try proxy to 1 on the express app
HRulier Aug 20, 2025
e4d67e0
fix(auth-google): remove logs
HRulier Aug 20, 2025
e4f94ec
fix(sendDailyEmailToUsers): add logs
HRulier Aug 21, 2025
ef3802c
feat(send daily reminders): remove startOfDay and endOfDay function
HRulier Aug 21, 2025
17abf5b
feat(sendDailyEmailToUsers): try to use timezone
HRulier Aug 21, 2025
1ad39c4
feat(sendDailyEmailToUsers): refactor to handle user timezone
HRulier Aug 21, 2025
50da572
feat(auth google): add support for set timezone when create user
HRulier Aug 21, 2025
4813369
feat: add auth with slack endpoint
HRulier Sep 1, 2025
58e97e3
Merge branch 'feature/auth-slack' into development
HRulier Sep 1, 2025
4447a1a
fix(auth-slack): wrong callback url
HRulier Sep 1, 2025
ee1e2ac
fix: SlackStrategy (passport) issue with scope
HRulier Sep 2, 2025
004b026
feat: endpoint get userId from slackId
HRulier Sep 2, 2025
cf4d29b
fix: fix schema for slack id
HRulier Sep 2, 2025
e0ced53
fix: fix schema for slack id
HRulier Sep 2, 2025
4968ef5
feat(user-model): add pproperty slackId
HRulier Sep 2, 2025
ff6d5ce
feat(post tasks/bulk): create endpoint for n8n automation to post mul…
HRulier Sep 3, 2025
c77ce2b
feat(operations): create model operation, schema, types and test, POS…
HRulier Sep 5, 2025
b95a709
Merge pull request #1 from HRulier/feature/operations
HRulier Sep 5, 2025
38b16ec
feat(operations): handle generate shortId, create endpoint PATCH /ope…
HRulier Sep 5, 2025
3d57aad
feat(operations): add test for PATCH
HRulier Sep 5, 2025
8436452
Merge pull request #2 from HRulier/feature/operations-validation
HRulier Sep 5, 2025
7123ded
fix: POST /operations, fix find user
HRulier Jan 22, 2026
b098e2e
feat(task): add support for priority (low, medium, high)
HRulier Jan 23, 2026
8d38d44
Merge pull request #3 from HRulier/feature/task-priorities
HRulier Jan 23, 2026
b237160
fix: set cors origin with env vars
HRulier Apr 7, 2026
eaf54b9
feat: revelant README.md and add description and author into package.…
HRulier Apr 7, 2026
47d2937
fix(server): parse env vars for cors origin
HRulier Apr 7, 2026
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ node_modules/
dist/
.DS_Store
structure.md
CLAUDE.md
CLAUDE.md

/docs/*
87 changes: 79 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,83 @@
# Build and run the API container with host networking
# Todo API

RESTful API for a Todo application built with **Express.js**, **TypeScript**, and **MongoDB**.

## Stack

- **Runtime**: Node.js + TypeScript
- **Framework**: Express.js
- **Database**: MongoDB + Mongoose
- **Auth**: Passport.js — Local, JWT, Google OAuth2
- **Validation**: Zod + OpenAPI auto-generation
- **Email**: React Email + Resend
- **Testing**: Jest + Supertest

## Getting Started

```bash
# Install dependencies
npm install

# Copy and fill environment variables
cp .env.example .env

# Start dev server
npm run dev
```

## Environment Variables

| Variable | Description |
|---|---|
| `MONGO_URI` | MongoDB connection string |
| `JWT_SECRET` | JWT signing secret |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
| `RESEND_API_KEY` | Resend email service key |
| `CORS_ORIGIN` | Allowed CORS origin |

## API Endpoints

| Prefix | Description |
|---|---|
| `/api/auth/*` | Register, login, OAuth, password reset |
| `/api/tasks/*` | Task CRUD with filtering & ordering |
| `/api/tags/*` | Tag CRUD with auto color assignment |
| `/api/operations/*` | Bulk task operations |
| `/api/jobs/*` | Scheduled jobs (daily reminders) |
| `/api-docs` | Swagger UI documentation |

## Scripts

```bash
npm run dev # Dev server with hot reload
npm run build # TypeScript build
npm start # Production server
npm test # Run tests
npm run test:coverage # Tests with coverage report
npm run email # Email template dev server
```

## Project Structure

```
src/
├── controllers/ # HTTP handlers
├── services/ # Business logic
├── models/ # Mongoose schemas
├── routes/ # Express routes
├── middlewares/ # Auth, validation, rate limiting
├── schemas/ # Zod schemas
└── openapi/ # OpenAPI doc generation
```

## Docker

```bash
docker build -t todo-api --target development .
docker run --name todo-api --network host -v .:/app -v /app/node_modules todo-api
```

docker run --name todo-api --network host \
-v .:/app \
-v /app/node_modules \
todo-api
## Author

docker start todo-api
docker stop todo-api
docker logs -f todo-api
[HRulier](https://github.com/HRulier)
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default {
testMatch: ["**/tests/**/*.test.ts"],
moduleNameMapper: {
"^~/(.*)$": "<rootDir>/src/$1",
"nanoid": "<rootDir>/tests/__mocks__/nanoid.js",
},
setupFilesAfterEnv: ["<rootDir>/tests/setup.ts"],
};
40 changes: 34 additions & 6 deletions package-lock.json

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

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
"type": "git",
"url": "git+https://github.com/HRulier/todo-api.git"
},
"author": "",
"author": "HRulier",
"license": "ISC",
"bugs": {
"url": "https://github.com/HRulier/todo-api/issues"
},
"homepage": "https://github.com/HRulier/todo-api#readme",
"description": "",
"description": "RESTful API for a Todo application — Express.js, TypeScript, MongoDB with JWT & OAuth2 authentication",
"devDependencies": {
"@types/cookie-parser": "^1.4.8",
"@types/cors": "^2.8.17",
Expand Down Expand Up @@ -64,11 +64,13 @@
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"nanoid": "^5.1.5",
"nodemon": "^3.1.9",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-slack-oauth2": "^1.2.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"resend": "^4.1.2",
Expand Down
73 changes: 73 additions & 0 deletions src/config/passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import User from "~/models/user";
import passportJWT from "passport-jwt";
import passportLocal from "passport-local";
import passportGoogleOAuth20 from "passport-google-oauth20";
import passportSlackOAuth20 from "passport-slack-oauth2";

import dotenv from "dotenv";
import dotEnvConfig from "./dot-env";
Expand All @@ -14,6 +15,7 @@ dotenv.config(dotEnvConfig);
const { Strategy: LocalStrategy } = passportLocal;
const { Strategy: JwtStrategy, ExtractJwt } = passportJWT;
const { Strategy: GoogleStrategy } = passportGoogleOAuth20;
const { Strategy: SlackStrategy } = passportSlackOAuth20;

// Setting username field to email rather than username
const localOptions = {
Expand Down Expand Up @@ -145,6 +147,7 @@ const googleStrategy = new GoogleStrategy(
firstName: profile.name?.familyName || "Non renseigné",
lastName: profile.name?.givenName || "Non renseigné",
},
timezone: stateData?.timezone,
});

// Pass both user and state data to the callback
Expand All @@ -156,6 +159,76 @@ const googleStrategy = new GoogleStrategy(
}
);

// Slack 0Auth2

const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID as string;
const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET as string;

const slackStrategy = new SlackStrategy(
{
clientID: SLACK_CLIENT_ID,
clientSecret: SLACK_CLIENT_SECRET,
callbackURL: `${process.env.API_URL}/auth/slack/callback`,
passReqToCallback: true,
scope: ["identity.basic", "email"],
},
async (
req: any,
accessToken: any,
refreshToken: any,
profile: any,
done: any
) => {
let stateData = null;

// Extract and decode state parameter if present
if (req.query.state) {
try {
// Validate state parameter as string because it's base64url
if (typeof req.query.state !== "string") {
throw new Error("Invalid state parameter format");
}

const decodedState = Buffer.from(
req.query.state,
"base64url"
).toString();

const stateObject = JSON.parse(decodedState);

if (!stateObject.data || typeof stateObject.data !== "object") {
throw new Error("Missing or invalid data in state");
}

stateData = stateObject.data;
} catch (stateError: any) {
console.error("OAuth state validation failed:", {
error: stateError.message,
stateLength: req.query.state?.length || 0,
userAgent: req.get("User-Agent"),
ip: req.ip || req.connection.remoteAddress,
});

// For security reasons, don't expose detailed error messages
// Continue without state data - don't fail the authentication
stateData = null;
}
}
const user = await findOrCreateUser({
email: profile.user.email,
slackId: profile.user.id,
profile: {
firstName: profile.user.name || "Non renseigné",
lastName: "Non renseigné",
},
timezone: stateData?.timezone,
});

done(null, { user });
}
);

passport.use(jwtLogin);
passport.use(localLogin);
passport.use(googleStrategy);
passport.use(slackStrategy);
Loading
Loading