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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Node / backend artifacts
node_modules/
backend/node_modules/
backend/.env
backend/.env.local
backend/.env.test
backend/prisma/*.db
backend/prisma/dev.db-journal
coverage/
.vscode/
.DS_Store
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

[English](README_en.md)

## Backend API

新增的 [`backend/`](backend) 目录包含一个基于 Express + Prisma 的用户/好友管理接口,提供注册、登录、资料维护、搜索、好友请求与列表等能力,使用 MySQL 作为数据源并通过 Vitest + Supertest + Testcontainers 覆盖主要场景。详细部署、迁移命令与请求/响应示例见 [`backend/README.md`](backend/README.md)。

- ✅ 包含
- ⏹ 仅部分包含
- ❌ 不包含
Expand Down
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DATABASE_URL="mysql://root:password@localhost:3306/user_friends"
PORT=4000
JWT_ACCESS_SECRET="change-me-access-secret-at-least-32-characters"
JWT_ACCESS_EXPIRES_IN="15m"
JWT_REFRESH_SECRET="change-me-refresh-secret-at-least-32-characters"
JWT_REFRESH_EXPIRES_IN="7d"
222 changes: 222 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# User & Friends Backend

An Express + Prisma API that powers user authentication, profile management, search, and friendship workflows on top of a MySQL database.

## Stack

- **Runtime:** Node.js 20
- **Framework:** Express 5
- **ORM:** Prisma 5 (MySQL)
- **Auth:** JWT access & refresh tokens with bcrypt hashing
- **Validation:** Zod
- **Tests:** Vitest + Supertest + Testcontainers (spins up a disposable MySQL 8 container)

## Quick start

```bash
cd backend
cp .env.example .env # update secrets + database url
npm install
npm run prisma:generate
npm run prisma:migrate # runs prisma migrate dev (requires the DB to be reachable)
npm run dev # or `npm start`
```

### Local MySQL via Docker

```bash
docker run --name user-friends-mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=user_friends \
-p 3306:3306 -d mysql:8.0
```

Point `DATABASE_URL` in `.env` at `mysql://root:secret@localhost:3306/user_friends` and then run `npm run prisma:migrate` to apply the schema. The initial migration lives at `prisma/migrations/20231121000000_init`.

### Environment variables

| Name | Description |
| --- | --- |
| `DATABASE_URL` | MySQL connection string used by Prisma |
| `PORT` | Port for the HTTP server (defaults to 3000) |
| `JWT_ACCESS_SECRET` | Secret used to sign short-lived access tokens (32+ chars recommended) |
| `JWT_ACCESS_EXPIRES_IN` | Access token TTL (e.g. `15m`) |
| `JWT_REFRESH_SECRET` | Secret for refresh tokens (keep separate from access secret) |
| `JWT_REFRESH_EXPIRES_IN` | Refresh token TTL (e.g. `7d`) |

### Available npm scripts

| Script | Description |
| --- | --- |
| `npm run dev` | Start the API locally |
| `npm test` | Run integration tests (needs Docker to launch MySQL) |
| `npm run prisma:migrate` | `prisma migrate dev` for local schema changes |
| `npm run prisma:deploy` | Apply migrations without generating new ones (CI/prod) |
| `npm run prisma:generate` | Regenerate Prisma Client |

## API reference

All responses share the same envelope:

```json
{
"success": true,
"data": {}
}
```

Errors respond with `success: false`, an HTTP status code, and `message` plus optional `errors` array.

### Auth

#### `POST /api/auth/register`

Request body:

```json
{
"email": "ada@example.com",
"username": "ada",
"password": "MySecurePass123",
"name": "Ada Lovelace",
"bio": "optional",
"avatarUrl": "https://example.com/avatar.png"
}
```

Response (201):

```json
{
"success": true,
"data": {
"user": { "id": "...", "email": "ada@example.com", "username": "ada", "name": "Ada Lovelace", "bio": "optional", "avatarUrl": "https://example.com/avatar.png", "createdAt": "2024-01-01T00:00:00.000Z", "updatedAt": "2024-01-01T00:00:00.000Z" },
"tokens": {
"accessToken": "<jwt>",
"refreshToken": "<jwt>",
"expiresIn": "15m",
"refreshExpiresIn": "7d"
}
}
}
```

#### `POST /api/auth/login`

Body:

```json
{
"identifier": "ada", // username or email
"password": "MySecurePass123"
}
```

Returns the same shape as registration.

#### `POST /api/auth/refresh`

Body:

```json
{ "refreshToken": "<jwt>" }
```

Returns a fresh `accessToken` and `refreshToken`. Old refresh tokens are invalidated immediately.

### User profile

> Requires `Authorization: Bearer <accessToken>` header.

#### `GET /api/users/me`

Returns the authenticated user profile.

#### `PUT /api/users/me`

Payload (any combination of fields):

```json
{ "name": "Ada L.", "bio": "Updated bio", "avatarUrl": "https://..." }
```

#### `GET /api/users/search`

Query params: `q` (optional search term), `page` (default 1), `pageSize` (default 20, max 50).

Response:

```json
{
"success": true,
"data": {
"items": [ { "id": "...", "username": "bobsmith", "name": "Bob Smith", ... } ],
"meta": { "page": 1, "pageSize": 10, "total": 1, "totalPages": 1 }
}
}
```

### Friendships

> All routes below require auth.

#### `POST /api/friends/requests`

Body:

```json
{ "userId": "target-user-id" }
```

Creates a pending friend request.

#### `PATCH /api/friends/requests/:id`

Body:

```json
{ "action": "ACCEPT" } // or DECLINE
```

Accepting automatically creates a friendship record.

#### `DELETE /api/friends/:id`

Removes the friendship with the specified user id.

#### `GET /api/friends`

Query params: `page`, `pageSize` (same semantics as `/search`).

Response:

```json
{
"success": true,
"data": {
"items": [
{
"friend": { "id": "...", "username": "receiverUser", "name": "Receiver" },
"since": "2024-01-01T00:00:00.000Z"
}
],
"meta": { "page": 1, "pageSize": 10, "total": 1, "totalPages": 1 }
}
}
```

### Error example

```json
{
"success": false,
"message": "Validation failed",
"errors": [
{ "path": "email", "message": "Invalid email" }
]
}
```

## Testing

Integration tests live under `tests/integration` and rely on Docker. Running `npm test` will automatically launch a temporary MySQL container via Testcontainers, apply Prisma migrations, run the Vitest suite, and tear everything down. This provides coverage for the full happy-path plus several failure scenarios.
Loading