Skip to content

Commit a8b96e7

Browse files
committed
Add local username/password auth
1 parent 96531af commit a8b96e7

11 files changed

Lines changed: 291 additions & 0 deletions

File tree

__tests__/routes.test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const request = require('supertest');
22
const express = require('express');
3+
const session = require('express-session');
34
const routes = require('../src/routes');
45
const { initializeTestDb } = require('./test-database');
56

@@ -13,6 +14,15 @@ function createTestApp() {
1314
const app = express();
1415
app.use(express.json());
1516
app.use(express.urlencoded({ extended: true }));
17+
18+
app.use(
19+
session({
20+
secret: 'test-session-secret',
21+
resave: false,
22+
saveUninitialized: false,
23+
cookie: { httpOnly: true },
24+
})
25+
);
1626

1727
// Simple mock for res.render
1828
app.use((req, res, next) => {
@@ -65,6 +75,37 @@ describe('Routes', () => {
6575
expect(recipe.title).toBe(newRecipe.title);
6676
});
6777

78+
test('POST /register should create a new user and redirect to profile', async () => {
79+
const response = await request(app)
80+
.post('/register')
81+
.send({ username: 'testuser', password: 'password123' });
82+
83+
expect(response.status).toBe(302);
84+
expect(response.headers.location).toBe('/profile');
85+
86+
const user = await db.get('SELECT * FROM users WHERE username = ?', ['testuser']);
87+
expect(user).toBeDefined();
88+
});
89+
90+
test('POST /login should authenticate and redirect to profile', async () => {
91+
await request(app)
92+
.post('/register')
93+
.send({ username: 'loginuser', password: 'password123' });
94+
95+
const response = await request(app)
96+
.post('/login')
97+
.send({ username: 'loginuser', password: 'password123' });
98+
99+
expect(response.status).toBe(302);
100+
expect(response.headers.location).toBe('/profile');
101+
});
102+
103+
test('GET /profile should redirect to login when not authenticated', async () => {
104+
const response = await request(app).get('/profile');
105+
expect(response.status).toBe(302);
106+
expect(response.headers.location).toBe('/login');
107+
});
108+
68109
test('POST /recipes/:id/delete should delete a recipe', async () => {
69110
// 先插入一条菜谱
70111
const recipe = {

__tests__/test-database.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ async function createTables(db) {
2323
ingredients TEXT,
2424
method TEXT
2525
)`)
26+
27+
// Create the 'users' table for authentication
28+
await db.exec(`CREATE TABLE IF NOT EXISTS users (
29+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
30+
username TEXT NOT NULL UNIQUE,
31+
password_hash TEXT NOT NULL
32+
)`)
2633
}
2734

2835
module.exports = { getTestDbConnection, initializeTestDb }

index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const express = require('express')
22
const exphbs = require('express-handlebars')
3+
const session = require('express-session')
34
const { initializeDb } = require('./src/database')
45
const routes = require('./src/routes')
56

@@ -10,6 +11,20 @@ app.use(express.json())
1011
app.use(express.urlencoded({ extended: true }))
1112
app.use(express.static('public'))
1213

14+
app.use(
15+
session({
16+
secret: process.env.SESSION_SECRET || 'dev-session-secret',
17+
resave: false,
18+
saveUninitialized: false,
19+
cookie: { httpOnly: true },
20+
})
21+
)
22+
23+
app.use((req, res, next) => {
24+
res.locals.currentUser = req.session.user || null
25+
next()
26+
})
27+
1328
app.engine(
1429
'hbs',
1530
exphbs.engine({

package-lock.json

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
"author": "Microsoft",
1313
"license": "MIT",
1414
"dependencies": {
15+
"bcryptjs": "^2.4.3",
1516
"express": "^5.1.0",
1617
"express-handlebars": "^7.1.3",
18+
"express-session": "^1.17.3",
1719
"sqlite": "^5.1.1",
1820
"sqlite3": "^5.1.7"
1921
},

src/database/schema.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ async function createTables(db) {
77
ingredients TEXT,
88
method TEXT
99
)`)
10+
11+
// Create the 'users' table for authentication
12+
await db.exec(`CREATE TABLE IF NOT EXISTS users (
13+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
14+
username TEXT NOT NULL UNIQUE,
15+
password_hash TEXT NOT NULL
16+
)`)
1017
}
1118

1219
module.exports = { createTables }

src/routes.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
const express = require('express')
2+
const bcrypt = require('bcryptjs')
23
const { getDbConnection } = require('./database')
34

45
const router = express.Router()
56

7+
function requireAuth(req, res, next) {
8+
if (!req.session.user) {
9+
return res.redirect('/login')
10+
}
11+
return next()
12+
}
13+
614
router.get('/', (req, res) => {
715
res.render('home', { title: 'Recipe App' })
816
})
@@ -20,6 +28,66 @@ router.get('/recipes/:id', async (req, res) => {
2028
res.render('recipe', { recipe })
2129
})
2230

31+
router.get('/register', (req, res) => {
32+
res.render('register')
33+
})
34+
35+
router.post('/register', async (req, res) => {
36+
const db = await getDbConnection()
37+
const { username, password } = req.body
38+
39+
if (!username || !password) {
40+
return res.render('register', { error: 'Username and password are required.' })
41+
}
42+
43+
const existingUser = await db.get('SELECT * FROM users WHERE username = ?', [username])
44+
if (existingUser) {
45+
return res.render('register', { error: 'Username already exists.' })
46+
}
47+
48+
const passwordHash = await bcrypt.hash(password, 10)
49+
await db.run('INSERT INTO users (username, password_hash) VALUES (?, ?)', [username, passwordHash])
50+
const createdUser = await db.get('SELECT id, username FROM users WHERE username = ?', [username])
51+
req.session.user = { id: createdUser.id, username: createdUser.username }
52+
return res.redirect('/profile')
53+
})
54+
55+
router.get('/login', (req, res) => {
56+
res.render('login')
57+
})
58+
59+
router.post('/login', async (req, res) => {
60+
const db = await getDbConnection()
61+
const { username, password } = req.body
62+
63+
if (!username || !password) {
64+
return res.render('login', { error: 'Username and password are required.' })
65+
}
66+
67+
const user = await db.get('SELECT * FROM users WHERE username = ?', [username])
68+
if (!user) {
69+
return res.render('login', { error: 'Invalid username or password.' })
70+
}
71+
72+
const passwordMatches = await bcrypt.compare(password, user.password_hash)
73+
if (!passwordMatches) {
74+
return res.render('login', { error: 'Invalid username or password.' })
75+
}
76+
77+
req.session.user = { id: user.id, username: user.username }
78+
return res.redirect('/profile')
79+
})
80+
81+
router.post('/logout', (req, res) => {
82+
req.session.destroy(() => {
83+
res.redirect('/')
84+
})
85+
})
86+
87+
router.get('/profile', requireAuth, async (req, res) => {
88+
res.render('profile', { user: req.session.user })
89+
})
90+
2391
router.post('/recipes', async (req, res) => {
2492
const db = await getDbConnection()
2593
const { title, ingredients, method } = req.body

views/layouts/main.hbs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@
1313
<ul>
1414
<li><a href="/">Home</a></li>
1515
<li><a href="/recipes">All Recipes</a></li>
16+
{{#if currentUser}}
17+
<li><a href="/profile">Profile</a></li>
18+
<li>
19+
<form action="/logout" method="POST" style="display:inline;">
20+
<button type="submit" class="btn btn-link">Logout</button>
21+
</form>
22+
</li>
23+
{{else}}
24+
<li><a href="/login">Login</a></li>
25+
<li><a href="/register">Register</a></li>
26+
{{/if}}
1627
</ul>
1728
</nav>
1829
</header>

views/login.hbs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<div class="auth-container">
2+
<h1>Login</h1>
3+
4+
{{#if error}}
5+
<div class="alert alert-error">{{error}}</div>
6+
{{/if}}
7+
8+
<form action="/login" method="POST" class="auth-form">
9+
<div class="form-group">
10+
<label for="username">Username:</label>
11+
<input type="text" id="username" name="username" required>
12+
</div>
13+
14+
<div class="form-group">
15+
<label for="password">Password:</label>
16+
<input type="password" id="password" name="password" required>
17+
</div>
18+
19+
<div class="form-actions">
20+
<button type="submit" class="btn btn-primary">Login</button>
21+
<a href="/register" class="btn btn-secondary">Create Account</a>
22+
</div>
23+
</form>
24+
</div>

views/profile.hbs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div class="profile-container">
2+
<h1>Profile</h1>
3+
4+
{{#if user}}
5+
<p>Signed in as <strong>{{user.username}}</strong>.</p>
6+
<p>Your saved recipes will appear here once the favorites feature is added.</p>
7+
{{else}}
8+
<p>Please <a href="/login">login</a> to view your profile.</p>
9+
{{/if}}
10+
</div>

0 commit comments

Comments
 (0)