Skip to content
Merged
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
20 changes: 15 additions & 5 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
# Copy to .env and provide strong values before running in production

# Server Configuration
PORT=4000
JWT_SECRET=replace-with-secure-random-string

# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRES_IN=15m

KOOMPI_CLIENT_ID=your-client-id
KOOMPI_CLIENT_SECRET=your-client-secret
KOOMPI_REDIRECT_URI=http://localhost:4000/api/oauth/callback
# MongoDB Configuration
MONGODB_URI=mongodb://localhost:27017/bitriel

MONGODB_URI=mongodb://127.0.0.1:27017/bitriel?authSource=admin&readPreference=primary&directConnection=true&ssl=false
# Koompi OAuth Configuration
KOOMPI_CLIENT_ID=pk_xxxxxxxxxxxxxxxxxxxxxx
KOOMPI_CLIENT_SECRET=sk_xxxxxxxxxxxxxxxxxxxxxx
KOOMPI_REDIRECT_URI=http://localhost:4000/api/oauth/callback
KOOMPI_MOBILE_REDIRECT_URI=http://localhost:4000/api/oauth/callback?platform=mobile

# Frontend Configuration
FRONTEND_URL=http://localhost:5173
FRONTEND_CALLBACK_PATH=/oauth/callback



177 changes: 166 additions & 11 deletions apps/backend/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,172 @@
# @bitriel/backend
# Bitriel Backend API

Simple Express API with JWT auth helpers.
Node.js + Express + TypeScript backend API for Bitriel digital wallet application with Koompi OAuth integration.

## Getting started
## Features

1. Copy `.env.example` to `.env` and set a strong `JWT_SECRET`.
2. Install dependencies from the repo root: `pnpm install`.
3. Start the API: `pnpm --filter @bitriel/backend dev`.
- 🔐 Koompi OAuth 2.0 + PKCE authentication
- 🎫 JWT token-based authorization
- 👤 User profile management
- 🗄️ MongoDB database integration
- 📱 Separate endpoints for web and mobile platforms
- 🛡️ TypeScript for type safety
- 🏗️ Clean architecture with service layer

## Routes
## Prerequisites

- `GET /health` – readiness probe.
- `POST /auth/token` – provide `{ "userId": "<id>", "roles": ["admin"] }` to receive a signed JWT.
- `GET /me` – attach `Authorization: Bearer <token>` to inspect the decoded claims.
- Node.js 18+ and pnpm 9+
- MongoDB 5+ (local or cloud)
- Koompi OAuth credentials (client ID & secret)

`JWT_EXPIRES_IN` (default `15m`) controls token lifetime.
## Quick Start

### 1. Install Dependencies

```bash
cd apps/backend
pnpm install
```

### 2. Environment Configuration

Copy `.env.example` to `.env`:

```bash
cp .env.example .env
```

Edit `.env` with your configuration:

```env
# Server
PORT=4000

# JWT
JWT_SECRET=your-super-secret-jwt-key-change-this
JWT_EXPIRES_IN=7d

# MongoDB
MONGODB_URI=mongodb://localhost:27017/bitriel

# Koompi OAuth
KOOMPI_CLIENT_ID=your-koompi-client-id
KOOMPI_CLIENT_SECRET=your-koompi-client-secret
KOOMPI_REDIRECT_URI=http://localhost:4000/api/oauth/callback
KOOMPI_MOBILE_REDIRECT_URI=http://localhost:4000/api/oauth/callback?platform=mobile

# Frontend
FRONTEND_URL=http://localhost:3000
FRONTEND_CALLBACK_PATH=/oauth/callback
```

### 3. Start MongoDB

```bash
# Using Docker
docker run -d -p 27017:27017 --name mongodb mongo:latest

# Or use MongoDB Atlas (cloud)
# Update MONGODB_URI in .env with your Atlas connection string
```

### 4. Run Development Server

```bash
pnpm dev
```

Server will start at `http://localhost:4000`

## API Endpoints

### Authentication

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/oauth/login` | GET | Initiates OAuth flow (query: `?platform=mobile\|web`) |
| `/api/oauth/callback` | GET | OAuth callback (handles both web and mobile via `?platform=`) |
| `/api/auth/me` | GET | Get authenticated user profile (requires JWT) |

### Testing Endpoints

```bash
# Test OAuth login (redirects to Koompi)
curl http://localhost:4000/api/oauth/login?platform=mobile

# Test user profile (requires valid JWT)
curl http://localhost:4000/api/auth/me \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```

## Project Structure

```
apps/backend/src/
├── controllers/ # HTTP request handlers
│ ├── oauthController.ts
│ └── userController.ts
├── services/ # Business logic layer
│ ├── oauthService.ts
│ └── userService.ts
├── models/ # MongoDB schemas
│ └── User.ts
├── middleware/ # Express middleware
│ └── auth.ts
├── routes/ # API route definitions
│ ├── oauthRoutes.ts
│ └── userRoutes.ts
├── types/ # TypeScript types
│ └── oauth.ts
├── utils/ # Utility functions
│ └── jwt.ts
├── config.ts # Environment config
└── index.ts # App entry point
```

## Development

### Available Scripts

```bash
pnpm dev # Start development server with hot reload
pnpm build # Build for production
pnpm start # Start production server
pnpm lint # Run ESLint
```

## Documentation

- **Full OAuth Integration Guide:** See [KOOMPI_OAUTH_INTEGRATION.md](../../docs/KOOMPI_OAUTH_INTEGRATION.md)
- **API Documentation:** See [API Endpoints](#api-endpoints) above

## Troubleshooting

### Common Issues

1. **MongoDB Connection Failed**
- Check MongoDB is running: `docker ps`
- Verify `MONGODB_URI` in `.env`

2. **OAuth Redirect Mismatch**
- Verify redirect URIs in Koompi OAuth dashboard
- Ensure exact match with `.env` values

3. **Invalid JWT Token**
- Check `JWT_SECRET` is set
- Verify token format: `Bearer <token>`

## Security

- ✅ Never commit `.env` files
- ✅ Use strong JWT secrets (32+ characters)
- ✅ Enable HTTPS in production
- ✅ Validate all user inputs

## Support

For detailed integration guide and troubleshooting:
- See [docs/KOOMPI_OAUTH_INTEGRATION.md](../../docs/KOOMPI_OAUTH_INTEGRATION.md)

---

**Last Updated:** 2025-01-21
2 changes: 2 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"dependencies": {
"@bitriel/http-client": "workspace:*",
"@koompi/oauth": "^1.0.4",
"@types/cookie-parser": "^1.4.10",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.2",
Expand Down
58 changes: 24 additions & 34 deletions apps/backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,40 @@
import cors from 'cors'
import express from 'express'
import jwt from 'jsonwebtoken'
import morgan from 'morgan'
import { config } from './config.js'
import { authenticate, type AuthenticatedRequest } from './middleware/authenticate.js'
import oauthRoutes from './routes/oauthRoutes.js'
import userRoutes from './routes/userRoutes.js'

const app = express()

app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(morgan('dev'))
import cors from 'cors';
import express from 'express';
import jwt from 'jsonwebtoken';
import morgan from 'morgan';
import { config } from './config.js';
import oauthRoutes from './routes/oauthRoutes.js';
import userRoutes from './routes/userRoutes.js';

const app = express();

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(morgan('dev'));

app.get('/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.post('/auth/token', (req, res) => {
const { userId, roles = [] } = req.body as { userId?: string; roles?: string[] }
const { userId, roles = [] } = req.body as { userId?: string; roles?: string[] };

if (!userId) {
return res.status(400).json({ error: 'userId is required' })
return res.status(400).json({ error: 'userId is required' });
}

const token = jwt.sign({ sub: userId, roles }, config.jwt.secret, {
expiresIn: config.jwt.expiresIn,
})

return res.json({ token, expiresIn: config.jwt.expiresIn })
})

app.get('/me', authenticate, (req, res) => {
const { user } = req as AuthenticatedRequest
});

return res.json({
userId: user?.sub,
claims: user,
})
})
return res.json({ token, expiresIn: config.jwt.expiresIn });
});

// OAuth routes
app.use('/api/oauth', oauthRoutes)
app.use('/api/oauth', oauthRoutes);

// User routes
app.use('/api/auth', userRoutes)
app.use('/api/auth', userRoutes);

export default app
export default app;
17 changes: 9 additions & 8 deletions apps/backend/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { SignOptions } from 'jsonwebtoken'
import type { SignOptions } from 'jsonwebtoken';

const ensure = (value: string | undefined, key: string) => {
if (!value) {
throw new Error(`Missing required environment variable: ${key}`)
throw new Error(`Missing required environment variable: ${key}`);
}
return value
}
return value;
};

const jwtSecret = ensure(process.env.JWT_SECRET, 'JWT_SECRET')
const jwtExpiresIn = (process.env.JWT_EXPIRES_IN ?? '15m') as SignOptions['expiresIn']
const jwtSecret = ensure(process.env.JWT_SECRET, 'JWT_SECRET');
const jwtExpiresIn = (process.env.JWT_EXPIRES_IN ?? '15m') as SignOptions['expiresIn'];

export const config = {
server: {
Expand All @@ -25,11 +25,12 @@ export const config = {
clientId: ensure(process.env.KOOMPI_CLIENT_ID, 'KOOMPI_CLIENT_ID'),
clientSecret: ensure(process.env.KOOMPI_CLIENT_SECRET, 'KOOMPI_CLIENT_SECRET'),
redirectUri: process.env.KOOMPI_REDIRECT_URI || 'http://localhost:4000/api/oauth/callback',
mobileRedirectUri: process.env.KOOMPI_MOBILE_REDIRECT_URI || 'http://localhost:4000/api/oauth/callback?platform=mobile',
},
frontend: {
url: process.env.FRONTEND_URL || 'http://localhost:5173',
callbackPath: process.env.FRONTEND_CALLBACK_PATH || '/oauth/callback',
},
}
};

export type JwtConfig = typeof config.jwt
export type JwtConfig = typeof config.jwt;
Loading
Loading