*/
97 | .messages p {
98 | font-size: 14px;
99 | font-weight: 400;
100 | line-height: 1.3;
101 | color: #d83f45;
102 | padding-left: 20px;
103 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23d83f45' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-alert-circle'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
104 | background-repeat: no-repeat;
105 | background-position: center left;
106 | }
107 |
--------------------------------------------------------------------------------
/routes/auth.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var passport = require('passport');
3 | var GoogleStrategy = require('passport-google-oauth20');
4 | var db = require('../db');
5 |
6 |
7 | // Configure the Facebook strategy for use by Passport.
8 | //
9 | // OAuth 2.0-based strategies require a `verify` function which receives the
10 | // credential (`accessToken`) for accessing the Facebook API on the user's
11 | // behalf, along with the user's profile. The function must invoke `cb`
12 | // with a user object, which will be set at `req.user` in route handlers after
13 | // authentication.
14 | passport.use(new GoogleStrategy({
15 | clientID: process.env['GOOGLE_CLIENT_ID'],
16 | clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
17 | callbackURL: '/oauth2/redirect/google',
18 | scope: [ 'profile' ],
19 | state: true
20 | },
21 | function(accessToken, refreshToken, profile, cb) {
22 | db.get('SELECT * FROM federated_credentials WHERE provider = ? AND subject = ?', [
23 | 'https://accounts.google.com',
24 | profile.id
25 | ], function(err, row) {
26 | if (err) { return cb(err); }
27 | if (!row) {
28 | db.run('INSERT INTO users (name) VALUES (?)', [
29 | profile.displayName
30 | ], function(err) {
31 | if (err) { return cb(err); }
32 | var id = this.lastID;
33 | db.run('INSERT INTO federated_credentials (user_id, provider, subject) VALUES (?, ?, ?)', [
34 | id,
35 | 'https://accounts.google.com',
36 | profile.id
37 | ], function(err) {
38 | if (err) { return cb(err); }
39 | var user = {
40 | id: id,
41 | name: profile.displayName
42 | };
43 | return cb(null, user);
44 | });
45 | });
46 | } else {
47 | db.get('SELECT rowid AS id, * FROM users WHERE rowid = ?', [ row.user_id ], function(err, row) {
48 | if (err) { return cb(err); }
49 | if (!row) { return cb(null, false); }
50 | return cb(null, row);
51 | });
52 | }
53 | });
54 | }));
55 |
56 | // Configure Passport authenticated session persistence.
57 | //
58 | // In order to restore authentication state across HTTP requests, Passport needs
59 | // to serialize users into and deserialize users out of the session. In a
60 | // production-quality application, this would typically be as simple as
61 | // supplying the user ID when serializing, and querying the user record by ID
62 | // from the database when deserializing. However, due to the fact that this
63 | // example does not have a database, the complete Facebook profile is serialized
64 | // and deserialized.
65 | passport.serializeUser(function(user, cb) {
66 | process.nextTick(function() {
67 | cb(null, { id: user.id, username: user.username, name: user.name });
68 | });
69 | });
70 |
71 | passport.deserializeUser(function(user, cb) {
72 | process.nextTick(function() {
73 | return cb(null, user);
74 | });
75 | });
76 |
77 |
78 | var router = express.Router();
79 |
80 | /* GET /login
81 | *
82 | * This route prompts the user to log in.
83 | *
84 | * The 'login' view renders an HTML page, which contain a button prompting the
85 | * user to sign in with Google. When the user clicks this button, a request
86 | * will be sent to the `GET /login/federated/accounts.google.com` route.
87 | */
88 | router.get('/login', function(req, res, next) {
89 | res.render('login');
90 | });
91 |
92 | /* GET /login/federated/accounts.google.com
93 | *
94 | * This route redirects the user to Google, where they will authenticate.
95 | *
96 | * Signing in with Google is implemented using OAuth 2.0. This route initiates
97 | * an OAuth 2.0 flow by redirecting the user to Google's identity server at
98 | * 'https://accounts.google.com'. Once there, Google will authenticate the user
99 | * and obtain their consent to release identity information to this app.
100 | *
101 | * Once Google has completed their interaction with the user, the user will be
102 | * redirected back to the app at `GET /oauth2/redirect/accounts.google.com`.
103 | */
104 | router.get('/login/federated/google', passport.authenticate('google'));
105 |
106 | /*
107 | This route completes the authentication sequence when Google redirects the
108 | user back to the application. When a new user signs in, a user account is
109 | automatically created and their Google account is linked. When an existing
110 | user returns, they are signed in to their linked account.
111 | */
112 | router.get('/oauth2/redirect/google', passport.authenticate('google', {
113 | successReturnToOrRedirect: '/',
114 | failureRedirect: '/login'
115 | }));
116 |
117 | /* POST /logout
118 | *
119 | * This route logs the user out.
120 | */
121 | router.post('/logout', function(req, res, next) {
122 | req.logout();
123 | res.redirect('/');
124 | });
125 |
126 | module.exports = router;
127 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var ensureLogIn = require('connect-ensure-login').ensureLoggedIn;
3 | var db = require('../db');
4 |
5 | var ensureLoggedIn = ensureLogIn();
6 |
7 | function fetchTodos(req, res, next) {
8 | db.all('SELECT rowid AS id, * FROM todos WHERE owner_id = ?', [
9 | req.user.id
10 | ], function(err, rows) {
11 | if (err) { return next(err); }
12 |
13 | var todos = rows.map(function(row) {
14 | return {
15 | id: row.id,
16 | title: row.title,
17 | completed: row.completed == 1 ? true : false,
18 | url: '/' + row.id
19 | }
20 | });
21 | res.locals.todos = todos;
22 | res.locals.activeCount = todos.filter(function(todo) { return !todo.completed; }).length;
23 | res.locals.completedCount = todos.length - res.locals.activeCount;
24 | next();
25 | });
26 | }
27 |
28 | var router = express.Router();
29 |
30 | /* GET home page. */
31 | router.get('/', function(req, res, next) {
32 | if (!req.user) { return res.render('home'); }
33 | next();
34 | }, fetchTodos, function(req, res, next) {
35 | res.locals.filter = null;
36 | res.render('index', { user: req.user });
37 | });
38 |
39 | router.get('/active', ensureLoggedIn, fetchTodos, function(req, res, next) {
40 | res.locals.todos = res.locals.todos.filter(function(todo) { return !todo.completed; });
41 | res.locals.filter = 'active';
42 | res.render('index', { user: req.user });
43 | });
44 |
45 | router.get('/completed', ensureLoggedIn, fetchTodos, function(req, res, next) {
46 | res.locals.todos = res.locals.todos.filter(function(todo) { return todo.completed; });
47 | res.locals.filter = 'completed';
48 | res.render('index', { user: req.user });
49 | });
50 |
51 | router.post('/', ensureLoggedIn, function(req, res, next) {
52 | req.body.title = req.body.title.trim();
53 | next();
54 | }, function(req, res, next) {
55 | if (req.body.title !== '') { return next(); }
56 | return res.redirect('/' + (req.body.filter || ''));
57 | }, function(req, res, next) {
58 | db.run('INSERT INTO todos (owner_id, title, completed) VALUES (?, ?, ?)', [
59 | req.user.id,
60 | req.body.title,
61 | req.body.completed == true ? 1 : null
62 | ], function(err) {
63 | if (err) { return next(err); }
64 | return res.redirect('/' + (req.body.filter || ''));
65 | });
66 | });
67 |
68 | router.post('/:id(\\d+)', ensureLoggedIn, function(req, res, next) {
69 | req.body.title = req.body.title.trim();
70 | next();
71 | }, function(req, res, next) {
72 | if (req.body.title !== '') { return next(); }
73 | db.run('DELETE FROM todos WHERE rowid = ? AND owner_id = ?', [
74 | req.params.id,
75 | req.user.id
76 | ], function(err) {
77 | if (err) { return next(err); }
78 | return res.redirect('/' + (req.body.filter || ''));
79 | });
80 | }, function(req, res, next) {
81 | db.run('UPDATE todos SET title = ?, completed = ? WHERE rowid = ? AND owner_id = ?', [
82 | req.body.title,
83 | req.body.completed !== undefined ? 1 : null,
84 | req.params.id,
85 | req.user.id
86 | ], function(err) {
87 | if (err) { return next(err); }
88 | return res.redirect('/' + (req.body.filter || ''));
89 | });
90 | });
91 |
92 | router.post('/:id(\\d+)/delete', ensureLoggedIn, function(req, res, next) {
93 | db.run('DELETE FROM todos WHERE rowid = ? AND owner_id = ?', [
94 | req.params.id,
95 | req.user.id
96 | ], function(err) {
97 | if (err) { return next(err); }
98 | return res.redirect('/' + (req.body.filter || ''));
99 | });
100 | });
101 |
102 | router.post('/toggle-all', ensureLoggedIn, function(req, res, next) {
103 | db.run('UPDATE todos SET completed = ? WHERE owner_id = ?', [
104 | req.body.completed !== undefined ? 1 : null,
105 | req.user.id
106 | ], function(err) {
107 | if (err) { return next(err); }
108 | return res.redirect('/' + (req.body.filter || ''));
109 | });
110 | });
111 |
112 | router.post('/clear-completed', ensureLoggedIn, function(req, res, next) {
113 | db.run('DELETE FROM todos WHERE owner_id = ? AND completed = ?', [
114 | req.user.id,
115 | 1
116 | ], function(err) {
117 | if (err) { return next(err); }
118 | return res.redirect('/' + (req.body.filter || ''));
119 | });
120 | });
121 |
122 | module.exports = router;
123 |
--------------------------------------------------------------------------------
/views/error.ejs:
--------------------------------------------------------------------------------
1 | <%= message %>
2 | <%= error.status %>
3 | <%= error.stack %>
4 |
--------------------------------------------------------------------------------
/views/home.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Express • TodoMVC
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 | todos helps you get things done
18 | Sign in
19 |
20 |
21 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Express • TodoMVC
7 |
8 |
9 |
10 |
11 |
12 |
13 |
24 |
34 | <% if (activeCount + completedCount > 0) { %>
35 |
36 |
41 |
42 | <% todos.forEach(function(todo) { %>
43 | - >
44 |
56 |
62 |
63 | <% }); %>
64 |
65 |
66 | <% } %>
67 | <% if (activeCount + completedCount > 0) { %>
68 |
91 | <% } %>
92 |
93 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/views/login.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Express • TodoMVC
7 |
8 |
9 |
10 |
11 |
12 |
13 | todos
14 | Sign in
15 | <% if (hasMessages) { %>
16 |
17 | <% messages.forEach(function(message) { %>
18 | <%= message %>
19 | <% }); %>
20 |
21 | <% } %>
22 | Sign in with Google
23 |
24 |
29 |
30 |
31 |
--------------------------------------------------------------------------------