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