This is a word cards management app built with Node.js
, Express
and Sqlite3
.
- Install dependencies
npm install
- Start the app
npm run dev
After that, you will see the terminal display:
App listening on the port 3000
- Generate
dist/
outputs
npm run build
- Start the app
npm run start
npm run test
When starting the app, the users table inserts 2 users by default
- User
Tom
, administrator identity - User
Mary
, normal user
All API are prefixed with /api/v1
, for example, you want to test login api, it's /api/v1/login
.
API | METHOD | Authorization | PARAMS/BODY | PERMISSION | DESCRIPTION |
---|---|---|---|---|---|
/login |
POST |
false |
{"uername": "Mary"} |
user 、admin |
User login |
/users |
GET |
true |
- |
admin |
Get all users |
/user/:id |
GET |
true |
- |
admin 、user |
Get user by id |
/revoke-token |
POST |
true |
{"userid": 1} |
admin |
Revoke user's token |
/cards |
POST |
true |
{"user_input": "test"} |
admin 、 user |
Create word card |
/cards |
GET |
true |
pageNumber=1&pageSize=10 |
admin 、 user |
Get cards with pagination |
/cards/:id |
GET |
true |
- |
admin 、 user |
Get card by id |
/cards/:id |
PUT |
true |
{"user_input": "modify01"} |
admin 、 user |
Modify card |
/cards/:id |
DELETE |
true |
- |
admin 、 user |
Delete card |
Notes: This feature has not yet been implemented
- When the user logs in for the first time, enter the username and password.
- After the server verifies that the username and password are correct, the
speakeasy
module is used to generate a two-factor authentication machine identificationcode
andsecret key
. - The server returns the identification
code
information to the user, prompts the user to open the Authenticator APP to scan the identification code and add it to the APP. - The user opens the Authenticator APP, scans the identification code, and binds the device. At this time, the APP can generate a 6-digit one-time password
(TOTP)
based on the secret key and the current time. - The server prompts the user to enter the
TOTP
generated by the APP to complete the two-factor verification. - The user copies the 6-bit
TOTP
from the APP, and the input is submitted to the server. The server side uses thespeakeasy
module to verify whether the incomingTOTP
is correct based on the bound key and time. - If the verification is passed, the two-factor authentication is completed, and the server generates a JWT containing the
verified flag
and returns it to the user. - When the user logs in later, after entering the username and password, the server verifies and generates the TOTP, and the user copies the TOTP submission from the APP.
- The server-side verifies the TOTP, and if it is correct, it can log in directly without scanning the code to bind.
- The user also needs to enter the current TOTP when invoking a function that requires two-factor authentication.
- The user table needs to add fields:
totp_secret
: Used to store TOTP keys generated by the speakeasy moduletotp_verified
: Marks if two-factor authentication is enabled- When the user logs in for the first time, totp_verified =
false
- After the user binds two-factor authentication, set the totp_verified to
true
- Follow-up login judgment totp_verified:
- If
false
, it means unbound, allow username and password to log in - If
true
, it means bound, and additional TOTP verification is required when logging in.
- If
- When the user turns off two-factor authentication, set the totp_verified to false
- When the user logs in for the first time, totp_verified =
- Login and two-factor authentication middleware can be designed as follows:
- User Login:
Verify Username Password
-- >Detect totp_verified
-- >If enabled, return scan binding prompt
- Two-factor authentication middleware:
verify username and password
-- >detect totp_verified
-- >generate TOTP and send it to the user
-- >verify the TOTP submitted by the user
- Insert authentication middleware before routes that require two-factor authentication, and verify TOTP
- After totp_verified is detected in some common login scenarios, the two-factor verification logic needs to be skipped 5.The generated JWT needs to include a custom claim that identifies two-factor verification
- Entry to enable and close two-factor authentication, storage totp_secret and update totp_verified
- Note the secure storage of TOTP keys, do not write to database cleartext
This app implements several strategies to handle high traffic and potential race conditions when updating data:
- For concurrent
POST
andPUT
requests tocreate/update
cards, a global taskQueue middleware is used. All write tasks are pushed to this queue and executed sequentially. The queue has a retry mechanism - if a task fails, it will be retried up to 2 times with a delay between retries.
this.router.post(
this.path,
...,
queueMiddleware(this.cards.createCard.bind(this.cards)),
);
-
The
PUT /cards
endpoint for updating cards uses database transactions to ensure data integrity under high concurrency. This guarantees that partial/incomplete updates do not happen. -
To protect against excessive requests and traffic spikes, a custom rate limiting middleware is used. If a client exceeds the request limit, a 429 Too Many Requests response is returned to inform the client to back off. This prevents any single client from overwhelming the application.
// Limit requests to 60 per minute
app.use(
createRateLimitMiddleware({
max: 60,
windowMs: 60 * 1000
})
);
Input validation is implemented using a middleware to protect the input value of user_input. It filters out special characters and prevents SQL injection.
For security purposes, a custom cors
middleware is implemented to configure the secure origin using environment variables.
this.app.use(cors({ origin: ORIGIN, credentials: CREDENTIALS }));
A security headers middleware, securityHeadersMiddleware
, is also written to enhance security measures.
export default function securityHeadersMiddleware(req, res, next) {
// Set the XSS guard header
res.setHeader('X-XSS-Protection', '1; mode=block');
// Disable content sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Remove leaky information
res.removeHeader('X-Powered-By');
next();
}