first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
node_modules
|
||||
users.db
|
||||
@@ -0,0 +1,114 @@
|
||||
# airclientauth
|
||||
|
||||
A simple, lightweight authentication system built with Node.js, Express, SQLite, and JSON Web Tokens (JWT).
|
||||
|
||||
## Features
|
||||
|
||||
- **CLI User Management**: Easily create users from the command line.
|
||||
- **First-Login Password Setup**: Users created via CLI do not have initial passwords. They set their password securely on their first login attempt.
|
||||
- **JWT Authentication**: Generates tokens for secure API communication.
|
||||
- **SQLite Storage**: Hashes and stores passwords safely using bcrypt and SQLite.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or download the repository.
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Start the Server
|
||||
|
||||
To start the API server on port 3000:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
*(The SQLite database `users.db` will be created automatically on the first run).*
|
||||
|
||||
### 2. Create a User (CLI)
|
||||
|
||||
You can create a new user account without a password using the built-in CLI tool:
|
||||
|
||||
```bash
|
||||
npm run airclientauth -- create myusername
|
||||
```
|
||||
|
||||
### 3. API Endpoints
|
||||
|
||||
#### Login / Set Password
|
||||
**`POST /login`**
|
||||
|
||||
- **First Login (Setting the Password)**
|
||||
When a user logs in for the first time, they must provide a password to set it.
|
||||
|
||||
*Request:*
|
||||
```json
|
||||
{
|
||||
"username": "myusername",
|
||||
"password": "mynewsecurepassword"
|
||||
}
|
||||
```
|
||||
*Response:*
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Password set successfully. Logged in.",
|
||||
"token": "eyJhbGciOiJIUzI..."
|
||||
}
|
||||
```
|
||||
*(Note: If you attempt to log in for the first time without a password, the server will return a `403 Forbidden` status with `{ "status": "require_password" }` to let the client know it needs to prompt the user).*
|
||||
|
||||
- **Subsequent Logins**
|
||||
Once the password is set, use the same endpoint to log in.
|
||||
|
||||
*Request:*
|
||||
```json
|
||||
{
|
||||
"username": "myusername",
|
||||
"password": "mynewsecurepassword"
|
||||
}
|
||||
```
|
||||
*Response:*
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Logged in successfully",
|
||||
"token": "eyJhbGciOiJIUzI..."
|
||||
}
|
||||
```
|
||||
|
||||
#### Verify Token
|
||||
**`GET /verify`**
|
||||
|
||||
Check if a provided JWT is still valid.
|
||||
|
||||
*Request Headers:*
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
*Response (Valid):*
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"user": "myusername"
|
||||
}
|
||||
```
|
||||
|
||||
*Response (Invalid):*
|
||||
```json
|
||||
{
|
||||
"valid": false,
|
||||
"error": "Invalid or expired token"
|
||||
}
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
- **Express**: Web framework for the API endpoints.
|
||||
- **bcryptjs**: Secure password hashing.
|
||||
- **jsonwebtoken (JWT)**: Token-based authentication.
|
||||
- **sqlite3 / sqlite**: File-based database storage.
|
||||
@@ -0,0 +1,126 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const sqlite3 = require('sqlite3');
|
||||
const { open } = require('sqlite');
|
||||
|
||||
const JWT_SECRET = 'super-secret-key-change-me';
|
||||
const DB_FILE = './users.db';
|
||||
|
||||
async function initDb() {
|
||||
const db = await open({
|
||||
filename: DB_FILE,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
await db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
username TEXT PRIMARY KEY,
|
||||
passwordHash TEXT
|
||||
)
|
||||
`);
|
||||
return db;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const db = await initDb();
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
const command = process.argv[2];
|
||||
if (command === 'create') {
|
||||
const username = process.argv[3];
|
||||
if (!username) {
|
||||
console.error('Please provide a username: npm run airclientauth -- create <user>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const user = await db.get('SELECT * FROM users WHERE username = ?', [username]);
|
||||
if (user) {
|
||||
console.error('User already exists.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await db.run('INSERT INTO users (username, passwordHash) VALUES (?, ?)', [username, null]);
|
||||
|
||||
console.log(`User '${username}' created successfully.`);
|
||||
console.log(`Waiting for first login to set the password.`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error('Unknown command. Available commands: create <user>');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).json({ error: 'Username is required' });
|
||||
}
|
||||
|
||||
const user = await db.get('SELECT * FROM users WHERE username = ?', [username]);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
if (!user.passwordHash) {
|
||||
if (password) {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
await db.run('UPDATE users SET passwordHash = ? WHERE username = ?', [hash, username]);
|
||||
|
||||
const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1h' });
|
||||
return res.json({
|
||||
status: 'success',
|
||||
message: 'Password set successfully. Logged in.',
|
||||
token
|
||||
});
|
||||
} else {
|
||||
return res.status(403).json({
|
||||
status: 'require_password',
|
||||
message: 'First login requires setting a password.'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!password) {
|
||||
return res.status(400).json({ error: 'Password is required' });
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({ error: 'Invalid password' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1h' });
|
||||
return res.json({
|
||||
status: 'success',
|
||||
message: 'Logged in successfully',
|
||||
token
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/verify', (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ valid: false, error: 'No token provided' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
return res.json({ valid: true, user: decoded.username });
|
||||
} catch (err) {
|
||||
return res.status(401).json({ valid: false, error: 'Invalid or expired token' });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
})();
|
||||
Generated
+1652
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "airclientauth",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"airclientauth": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^6.0.1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user