first commit

This commit is contained in:
Patrick
2026-06-07 20:42:34 +02:00
commit be434d7e6f
5 changed files with 1917 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.idea
node_modules
users.db
+114
View File
@@ -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.
+126
View File
@@ -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}`);
});
})();
+1652
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -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"
}
}