A note-taking app where every note can be shared with a single link. Built as a two-part project: a React client and a NestJS API, connected by cookie-based JWT authentication with refresh-token rotation and email confirmation.
| Feature | Description |
|---|---|
| Notes with rich text | Create and edit notes in a Quill editor and store them as HTML. |
| Share by link | Each note has an access mode — LINK (anyone with the URL can read it) or PRIVATE (author only). |
| Ownership rules | Only the author can rename, edit or delete a note; the API enforces it on every request instead of trusting the UI. |
| Accounts | Registration with a hashed password (bcrypt), e-mail confirmation link, sign-in, sign-out. |
| Session handling | Short-lived access token + 30-day refresh token; the refresh token lives in an httpOnly cookie, the client silently renews on 401 and replays the request. |
N-Note/
├── client/ # React 18 + TypeScript (Create React App)
│ └── src/
│ ├── api/ # axios instance: Bearer header + refresh interceptor
│ ├── components/ # Header, Note, Note editor, modals
│ ├── pages/ # CreateNote, Note, SignIn, SignUp
│ ├── redux/ # Redux Toolkit store (user slice)
│ └── styles/ # SCSS modules per component
└── server/ # NestJS 9 + Mongoose
└── src/
├── auth/ # sign-in, sign-out, refresh; jwt/mailer/mongo configs
├── user/ # registration, profile, e-mail activation, JWT guard
├── note/ # note CRUD + sharing
├── token/ # refresh-token persistence
├── mail/ # transactional e-mail (confirmation link)
├── schemas/ # mongoose schemas: user, note, token
└── decorators/ # @UserId() — pulls the user out of the JWT
POST /user/register— validates the payload, hashes the password, stores the account as inactive and sends a confirmation e-mail. A refresh token is returned in anhttpOnlycookie.GET /user/activate/:link— marks the account active and redirects the browser toCLIENT_URL.POST /auth/login— returns{ user, accessToken, refreshToken }and sets the refresh token as a cookie.- The client keeps the access token in memory/local storage and sends it as
Authorization: Bearer …. On401an axios interceptor callsGET /auth/refresh(cookie travels automatically), stores the new access token and retries the original request once (_isRetryflag prevents loops). POST /auth/logout— clears the cookie and removes the refresh token from the database, so the session cannot be resumed.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/user/register |
— | Create an account, send the confirmation e-mail |
GET |
/user/activate/:link |
— | Confirm the e-mail and redirect to the client |
GET |
/user/me |
Bearer | Current profile |
GET |
/user/:id |
— | Public profile of a note author |
POST |
/auth/login |
— | Sign in, returns the token pair |
GET |
/auth/refresh |
refresh cookie | Rotate the token pair |
POST |
/auth/logout |
refresh cookie | End the session |
GET |
/note/:id |
optional | Read a note (must be LINK when the caller is not the author) |
POST |
/note/create |
Bearer | Create a note |
POST |
/note/:id |
Bearer | Rename a note |
PATCH |
/note/:id |
Bearer | Update the note body |
DELETE |
/note/:id |
Bearer | Delete a note |
Client — React 18, TypeScript, Redux Toolkit, React Router 6, MUI, axios, react-hook-form, react-quill, SCSS modules, Create React App.
Server — NestJS 9, Mongoose (MongoDB), Passport JWT, @nestjs/jwt,
@nestjs-modules/mailer + nodemailer, bcrypt, class-validator / class-transformer,
cookie-parser.
cd server
npm installCreate server/.env:
MONGO_USER=your-atlas-user
MONGO_PASSWORD=your-atlas-password
JWT_KEY=long-random-secret
MAIL_HOST=smtp.example.com
MAIL_PORT=465
MAIL_USER=you@example.com
MAIL_PASSWORD=your-smtp-password
CLIENT_URL=http://localhost:3000npm run start:dev # http://localhost:4000cd client
npm install
npm start # http://localhost:3000The API URL is configured in client/src/api/axios.ts; the server allows the
http://localhost:3000 origin with credentials.
- Passwords are hashed with bcrypt; the refresh token is both stored in MongoDB
and delivered as an
httpOnlycookie, so a stolen local-storage value alone is not enough to keep a session. - Access modes live on the note document (
LINK/PRIVATE), which keeps the sharing rule in one place instead of spreading it across controllers. - The project intentionally keeps the client on Create React App — it is the original implementation and is left untouched so the diff history stays honest.