Skip to content

Commit 27ab673

Browse files
authored
feat: Sign in by Discord (#1347)
1 parent fb528c9 commit 27ab673

File tree

9 files changed

+121
-6
lines changed

9 files changed

+121
-6
lines changed

.env.example

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ RECAPTCHA_SECRET_KEY=87ehxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3298
2626
#
2727
ALPHA_VANTAGE_KEY=api-key
2828

29+
DISCORD_CLIENT_ID=discord-client-id/discord-app-id
30+
DISCORD_CLIENT_SECRET=discord-client-secret
31+
2932
FACEBOOK_ID=754220301289665
3033
FACEBOOK_SECRET=41860e58c256a3d7ad8267d3c1939a4a
3134
# FB Pixel ID is optional if you are trying to do customer rtracking

README.md

+18-6
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,15 @@ I also tried to make it as **generic** and **reusable** as possible to cover mos
7070
## Features
7171

7272
- Login
73-
- **Local Authentication** using Email and Password, as well as Passwordless
74-
- **OAuth 2.0 Authentication:** Sign in with Google, Facebook, X (Twitter), Twitch, Github
73+
- **Local Authentication** Sign in with Email and Password, Passwordless
74+
- **OAuth 2.0 Authentication:** Sign in with Google, Facebook, X (Twitter), Twitch, Github, Discord
7575
- **OpenID Connect:** Sign in with LinkedIn
7676
- **User Profile and Account Management**
7777
- Gravatar
7878
- Profile Details
79-
- Change Password
80-
- Forgot Password
81-
- Reset Password
79+
- Password management (Change, Reset, Forgot)
8280
- Verify Email
83-
- Link multiple OAuth strategies to one account
81+
- Link multiple OAuth provider accounts to one account
8482
- Delete Account
8583
- Contact Form (powered by SMTP via Sendgrid, Mailgun, AWS SES, etc.)
8684
- File upload
@@ -270,6 +268,20 @@ Obtain SMTP credentials from a provider for transactional emails. Set the SMTP_U
270268

271269
<hr>
272270

271+
<img src="https://cdn.worldvectorlogo.com/logos/discord-6.svg" height="50">
272+
273+
- Go to <a href="https://discord.com/developers/teams" target="_blank">Teams tab</a> in the Discord Developer Portal and create a new team. This allows you to manage your Discord applications under a team name instead of your personal account.
274+
- After creating a team, switch to the <a href="https://discord.com/developers/applications" target="_blank">Applications tab</a> in the Discord Developer Portal.
275+
- Click on **New Application** and give your app a name. When prompted, select your team as the owner.
276+
- In the left sidebar, click on **OAuth2** > **General**.
277+
- Copy the **Client ID** and **Client Secret** (you may need to "reset" the client secret to obtain it for the first time), then paste them into your `.env` file as `DISCORD_CLIENT_ID` and `DISCORD_CLIENT_SECRET`, or set them as environment variables.
278+
- In the left sidebar, click on **OAuth2** > **URL Generator**.
279+
- Under **Scopes**, select `identify` and `email`.
280+
- Under **Redirects**, add your BASE_URL value followed by `/auth/discord/callback` (i.e. `http://localhost:8080/auth/discord/callback`).
281+
- Save changes.
282+
283+
<hr>
284+
273285
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c7/HERE_logo.svg" height="75">
274286

275287
- Go to <a href="https://developer.here.com" target="_blank">https://developer.here.com</a>

app.js

+4
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ app.get('/auth/twitch', passport.authenticate('twitch'));
262262
app.get('/auth/twitch/callback', passport.authenticate('twitch', { failureRedirect: '/login' }), (req, res) => {
263263
res.redirect(req.session.returnTo || '/');
264264
});
265+
app.get('/auth/discord', passport.authenticate('discord'));
266+
app.get('/auth/discord/callback', passport.authenticate('discord', { failureRedirect: '/login' }), (req, res) => {
267+
res.redirect(req.session.returnTo || '/');
268+
});
265269

266270
/**
267271
* OAuth authorization routes. (API examples)

config/passport.js

+73
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,79 @@ const traktStrategyConfig = new OAuth2Strategy(
764764
passport.use('trakt', traktStrategyConfig);
765765
refresh.use('trakt', traktStrategyConfig);
766766

767+
/**
768+
* Sign in with Discord using OAuth2Strategy.
769+
*/
770+
const discordStrategyConfig = new OAuth2Strategy(
771+
{
772+
authorizationURL: 'https://discord.com/api/oauth2/authorize',
773+
tokenURL: 'https://discord.com/api/oauth2/token',
774+
clientID: process.env.DISCORD_CLIENT_ID,
775+
clientSecret: process.env.DISCORD_CLIENT_SECRET,
776+
callbackURL: `${process.env.BASE_URL}/auth/discord/callback`,
777+
scope: ['identify', 'email'].join(' '),
778+
state: generateState(),
779+
passReqToCallback: true,
780+
},
781+
async (req, accessToken, refreshToken, params, profile, done) => {
782+
try {
783+
// Fetch Discord profile using accessToken
784+
const response = await fetch('https://discord.com/api/users/@me', {
785+
headers: {
786+
Authorization: `Bearer ${accessToken}`,
787+
},
788+
});
789+
if (!response.ok) {
790+
return done(new Error('Failed to fetch Discord profile'));
791+
}
792+
const discordProfile = await response.json();
793+
794+
if (req.user) {
795+
const existingUser = await User.findOne({ discord: { $eq: discordProfile.id } });
796+
if (existingUser && existingUser.id !== req.user.id) {
797+
req.flash('errors', {
798+
msg: 'There is already a Discord account that belongs to you. Sign in with that account or delete it, then link it with your current account.',
799+
});
800+
return done(null, existingUser);
801+
}
802+
const user = await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, null, 'discord');
803+
user.discord = discordProfile.id;
804+
user.profile.name = user.profile.name || discordProfile.username;
805+
user.profile.picture = user.profile.picture || (discordProfile.avatar ? `https://cdn.discordapp.com/avatars/${discordProfile.id}/${discordProfile.avatar}.png` : undefined);
806+
await user.save();
807+
req.flash('info', { msg: 'Discord account has been linked.' });
808+
return done(null, user);
809+
}
810+
const existingUser = await User.findOne({ discord: { $eq: discordProfile.id } });
811+
if (existingUser) {
812+
return done(null, existingUser);
813+
}
814+
const existingEmailUser = await User.findOne({
815+
email: { $eq: discordProfile.email },
816+
});
817+
if (existingEmailUser) {
818+
req.flash('errors', {
819+
msg: 'There is already an account using this email address. Sign in to that account and link it with Discord manually from Account Settings.',
820+
});
821+
return done(null, existingEmailUser);
822+
}
823+
const user = new User();
824+
user.email = discordProfile.email;
825+
user.discord = discordProfile.id;
826+
req.user = user;
827+
await saveOAuth2UserTokens(req, accessToken, refreshToken, params.expires_in, null, 'discord');
828+
user.profile.name = discordProfile.username;
829+
user.profile.picture = discordProfile.avatar ? `https://cdn.discordapp.com/avatars/${discordProfile.id}/${discordProfile.avatar}.png` : undefined;
830+
await user.save();
831+
return done(null, user);
832+
} catch (err) {
833+
return done(err);
834+
}
835+
},
836+
);
837+
passport.use('discord', discordStrategyConfig);
838+
refresh.use('discord', discordStrategyConfig);
839+
767840
/**
768841
* Login Required middleware.
769842
*/

models/User.js

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const userSchema = new mongoose.Schema(
3030
quickbooks: String,
3131
tumblr: String,
3232
trakt: String,
33+
discord: String,
3334
tokens: Array,
3435

3536
profile: {

public/css/main.scss

+11
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@
5050
}
5151
}
5252

53+
.btn-discord {
54+
background-color: #5865f2;
55+
color: #fff !important;
56+
57+
&:hover,
58+
&:active {
59+
background-color: #4752c4;
60+
color: #fff !important;
61+
}
62+
}
63+
5364
// Add a space after fontawesome icons
5465
.iconpadding {
5566
padding-right: 6px !important;

test/.env.test

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ RECAPTCHA_SECRET_KEY=test_recaptcha_secret_key
1717

1818
ALPHA_VANTAGE_KEY=api-key
1919

20+
DISCORD_CLIENT_ID=discord-client-id/discord-app-id
21+
DISCORD_CLIENT_SECRET=discord-client-secret
22+
2023
FACEBOOK_ID=1234567890123456
2124
FACEBOOK_SECRET=test_facebook_secret
2225
FACEBOOK_PIXEL_ID=

views/account/login.pug

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ block content
4747
a.btn.btn-block.btn-github.btn-social(href='/auth/github')
4848
i.fab.fa-github.fa-sm
4949
| Sign in with GitHub
50+
a.btn.btn-block.btn-discord.btn-social(href='/auth/discord')
51+
i.fab.fa-discord
52+
| Log in with Discord
5053

5154
script.
5255
document.getElementById('loginByEmailLink').addEventListener('change', function () {

views/account/profile.pug

+5
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,8 @@ block content
151151
p.mb-1: a.text-danger(href='/account/unlink/trakt') Unlink your Trakt account
152152
else
153153
p.mb-1: a(href='/auth/trakt') Link your Trakt account
154+
.offset-sm-3.col-md-7.pl-2
155+
if user.discord
156+
p.mb-1: a.text-danger(href='/account/unlink/discord') Unlink your Discord account
157+
else
158+
p.mb-1: a(href='/auth/discord') Link your Discord account

0 commit comments

Comments
 (0)