Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d0857f0
Migrate to TS
Apr 20, 2026
fb6f68e
Update data table
Apr 20, 2026
3084db1
Migrate base code from scratch
Apr 21, 2026
c854244
Add response handler
Apr 21, 2026
37e1fec
Integrate route home
Apr 21, 2026
019a23c
Integrate not found handler
Apr 21, 2026
779cc62
Integrate db connection
Apr 21, 2026
f8f592a
Feat: Get Contact By ID
Apr 21, 2026
270e135
Docs: update env example
Apr 21, 2026
61d96df
Feat: Delete Contact By ID
Apr 21, 2026
e0303d9
Feat: register
Apr 21, 2026
bd4bc35
Feat: login
Apr 21, 2026
f59bf4d
Refactor: Service & naming
Apr 21, 2026
31c6676
Refactor: middleware
Apr 21, 2026
98e3719
Feat: create contact
Apr 21, 2026
0d20d35
Refactor: update type from String to string
Apr 21, 2026
aa1482e
Feat: update contact
Apr 21, 2026
da6f4c9
Fix: bug unique email
otakmager Apr 22, 2026
4085067
Docs: update env example
Apr 24, 2026
0ba962e
Merge branch 'dev' of https://github.com/otakmager/be-cms into dev
Apr 24, 2026
a29c0a3
Feat: rate limit
Apr 24, 2026
11fdb0a
Feat: security & performance
Apr 24, 2026
d4a5eb5
Feat: logging
Apr 24, 2026
3d7e81d
Refactor: add search + pretiier
otakmager Apr 26, 2026
96deacf
Feat: statistics
otakmager Apr 26, 2026
208dc85
Feat: implement admin guard middleware for access control
otakmager Apr 26, 2026
9905cf8
Feat: add lastLogin tracking and inactive user cleanup job
otakmager Apr 26, 2026
35dd18c
Feat: add Swagger documentation
otakmager Apr 26, 2026
17a4c06
Refactor: normalize paths in statistics tracking
otakmager Apr 26, 2026
05b5d8e
Test: add authentication tests
otakmager Apr 26, 2026
15d5af6
Test: add admin tests
otakmager Apr 26, 2026
7467d28
Test: add contact test + refactor redundant mock
Apr 28, 2026
b07e905
Test: add home test
Apr 28, 2026
28f3d66
Refactor: update coverage threshold to 70%
Apr 28, 2026
c842c5c
Refactor: update yml
Apr 28, 2026
4c40388
Refactor: update yml
Apr 28, 2026
047e131
Refactor: update yml
Apr 28, 2026
9058919
Refactor: update yml
Apr 28, 2026
3c976a3
Refactor: update yml
Apr 28, 2026
4e7028a
Refactor: update yml
Apr 28, 2026
e9ec634
Refactor: update yml
Apr 28, 2026
e087406
Refactor: update yml
Apr 28, 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
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DATABASE_URL=
SHADOW_DATABASE_URL=
DB_HOST=
DB_USER=
DB_PASSWORD=
DB_NAME=
PORT=
HOST=
JWT_SECRET_KEY=
ADMIN_SECRET_KEY=
CORS_ORIGIN=
NODE_ENV=
91 changes: 91 additions & 0 deletions .github/workflows/test-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Test CI - Pull Request Validation

on:
pull_request:
branches:
- main

jobs:
test:
name: Run Tests & Validate Coverage
runs-on: ubuntu-latest
permissions:
contents: read # Required to checkout the code
issues: write # Required to create comments on PRs
pull-requests: write # Required to set commit statuses
statuses: write # Required to set commit statuses (often implicitly covered by pull-requests: write, but good to be explicit)
env:
# Dummy URL for init db in test CI
DATABASE_URL: mysql://root:password@localhost:3306/be_cms
SHADOW_DATABASE_URL: mysql://root:password@localhost:3306/shadow_be_cms

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Generate Prisma Client
run: npm run gen

- name: Run tests with coverage
run: npm test -- --coverage --coverageReporters=json-summary --coverageReporters=text
continue-on-error: true

- name: Parse coverage and check threshold
id: coverage
run: |
# Read coverage.json and extract coverage percentage
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
echo "Coverage: $COVERAGE%"

# Check if coverage >= 70%
if (( $(echo "$COVERAGE >= 70" | bc -l) )); then
echo "✅ Coverage meets threshold (70%)"
echo "status=pass" >> $GITHUB_OUTPUT
exit 0
else
echo "❌ Coverage below threshold (70%)"
echo "status=fail" >> $GITHUB_OUTPUT
exit 1
fi
shell: bash

- name: Comment PR with test results
if: always()
uses: actions/github-script@v7
with:
script: |
const coverage = '${{ steps.coverage.outputs.coverage }}';
const status = '${{ steps.coverage.outputs.status }}';

const icon = status === 'pass' ? '✅' : '❌';
const threshold = 70;

const comment = `## ${icon} Test Coverage Report

- **Coverage**: ${coverage}%
- **Threshold**: ${threshold}%
- **Status**: ${status === 'pass' ? 'PASS ✅ - Ready to merge' : 'FAIL ❌ - Coverage below threshold'}

${status === 'pass' ? '**This PR can be merged!**' : '**Please improve test coverage to 70% or higher before merging.**'}
`;

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});

- name: Fail if coverage is below threshold
if: steps.coverage.outputs.status == 'fail'
run: exit 1
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,8 @@ lerna-debug.log

# System Files
.DS_Store
Thumbs.db
Thumbs.db
/src/generated/prisma

#Additional
stats.json
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
dist
build
prisma
package-lock.json
.env
.env.example
stats.json
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"useTabs": true,
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
52 changes: 52 additions & 0 deletions admin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
jest.mock('../service/stats.service', () => ({
getStats: jest.fn(),
}));

import request from 'supertest';
import express from 'express';
import * as StatsService from '../service/stats.service';

const adminRouter = require('../routes/admin.route').default;

const app = express();
app.use(express.json());
app.use('/admin', adminRouter);

describe('Admin Controller - API Stats', () => {
const ADMIN_KEY = 'super-secret-admin-key';

beforeAll(() => {
process.env.ADMIN_SECRET_KEY = ADMIN_KEY;
});

beforeEach(() => {
jest.clearAllMocks();
});

it('should return 401 if x-admin-key header is missing', async () => {
const res = await request(app).get('/admin/stats');

expect(res.status).toBe(401);
expect(res.body.success).toBe(false);
});

it('should return 401 if x-admin-key is incorrect', async () => {
const res = await request(app).get('/admin/stats').set('x-admin-key', 'wrong-password-key');

expect(res.status).toBe(401);
expect(res.body.success).toBe(false);
});

it('should return 200 and statistics when x-admin-key is valid', async () => {
const mockStats = { 'GET /': 10, 'POST /login': 2 };
(StatsService.getStats as jest.Mock).mockReturnValue(mockStats);

const res = await request(app).get('/admin/stats').set('x-admin-key', ADMIN_KEY);

expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.message).toBe('Statistics retrieved successfully');
expect(res.body.data).toEqual(mockStats);
expect(StatsService.getStats).toHaveBeenCalledTimes(1);
});
});
48 changes: 48 additions & 0 deletions docs/req_res/auth.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
=== REGISTER
/register
POST
body:
{
"username": "user",
"password": "yourpassword"
}
response success:
{
"success": true,
"message": "User registered successfully",
"data": {
"username": "user",
"createdAt": "2026-04-26T04:58:47.105Z",
"updatedAt": "2026-04-26T04:58:47.105Z"
}
}
response failed:
{
"success": false,
"code": 409,
"message": "User already exists"
}

=== LOGIN
/login
POST
body:
{
"username": "user",
"password": "yourpassword"
}
response success:
{
"success": true,
"message": "Login successful",
"data": {
"username": "user",
"token": "yourtoken"
}
}
response failed:
{
"success": false,
"code": 401,
"message": "Invalid username or password"
}
Loading
Loading