Skip to content

Commit 007d51e

Browse files
authored
feat: facebook login (danny-avila#820)
* Facebook strategy * Update user_auth_system.md * Update user_auth_system.md
1 parent a569020 commit 007d51e

File tree

23 files changed

+155
-27
lines changed

23 files changed

+155
-27
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ GOOGLE_CLIENT_ID=
229229
GOOGLE_CLIENT_SECRET=
230230
GOOGLE_CALLBACK_URL=/oauth/google/callback
231231

232+
# Facebook:
233+
# Add your Facebook Client ID and Secret here, you must register an app with Facebook to get these values
234+
# https://developers.facebook.com/
235+
FACEBOOK_CLIENT_ID=
236+
FACEBOOK_CLIENT_SECRET=
237+
FACEBOOK_CALLBACK_URL=/oauth/facebook/callback
238+
232239
# OpenID:
233240
# See OpenID provider to get the below values
234241
# Create random string for OPENID_SESSION_SECRET

api/models/User.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ const userSchema = mongoose.Schema(
6363
unique: true,
6464
sparse: true,
6565
},
66+
facebookId: {
67+
type: String,
68+
unique: true,
69+
sparse: true,
70+
},
6671
openidId: {
6772
type: String,
6873
unique: true,

api/server/routes/__tests__/config.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ afterEach(() => {
88
delete process.env.APP_TITLE;
99
delete process.env.GOOGLE_CLIENT_ID;
1010
delete process.env.GOOGLE_CLIENT_SECRET;
11+
delete process.env.FACEBOOK_CLIENT_ID;
12+
delete process.env.FACEBOOK_CLIENT_SECRET;
1113
delete process.env.OPENID_CLIENT_ID;
1214
delete process.env.OPENID_CLIENT_SECRET;
1315
delete process.env.OPENID_ISSUER;
@@ -31,6 +33,8 @@ describe.skip('GET /', () => {
3133
process.env.APP_TITLE = 'Test Title';
3234
process.env.GOOGLE_CLIENT_ID = 'Test Google Client Id';
3335
process.env.GOOGLE_CLIENT_SECRET = 'Test Google Client Secret';
36+
process.env.FACEBOOK_CLIENT_ID = 'Test Facebook Client Id';
37+
process.env.FACEBOOK_CLIENT_SECRET = 'Test Facebook Client Secret';
3438
process.env.OPENID_CLIENT_ID = 'Test OpenID Id';
3539
process.env.OPENID_CLIENT_SECRET = 'Test OpenID Secret';
3640
process.env.OPENID_ISSUER = 'Test OpenID Issuer';
@@ -51,6 +55,7 @@ describe.skip('GET /', () => {
5155
expect(response.body).toEqual({
5256
appTitle: 'Test Title',
5357
googleLoginEnabled: true,
58+
facebookLoginEnabled: true,
5459
openidLoginEnabled: true,
5560
openidLabel: 'Test OpenID',
5661
openidImageUrl: 'http://test-server.com',

api/server/routes/config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ router.get('/', async function (req, res) {
55
try {
66
const appTitle = process.env.APP_TITLE || 'LibreChat';
77
const googleLoginEnabled = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
8+
const facebookLoginEnabled =
9+
!!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET;
810
const openidLoginEnabled =
911
!!process.env.OPENID_CLIENT_ID &&
1012
!!process.env.OPENID_CLIENT_SECRET &&
@@ -27,6 +29,7 @@ router.get('/', async function (req, res) {
2729
return res.status(200).send({
2830
appTitle,
2931
googleLoginEnabled,
32+
facebookLoginEnabled,
3033
openidLoginEnabled,
3134
openidLabel,
3235
openidImageUrl,

api/server/routes/oauth.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ router.get(
3838
router.get(
3939
'/facebook',
4040
passport.authenticate('facebook', {
41-
scope: ['public_profile', 'email'],
41+
scope: ['public_profile'],
42+
profileFields: ['id', 'email', 'name'],
4243
session: false,
4344
}),
4445
);
@@ -49,7 +50,8 @@ router.get(
4950
failureRedirect: `${domains.client}/login`,
5051
failureMessage: true,
5152
session: false,
52-
scope: ['public_profile', 'email'],
53+
scope: ['public_profile'],
54+
profileFields: ['id', 'email', 'name'],
5355
}),
5456
(req, res) => {
5557
const token = req.user.generateToken();

api/strategies/facebookStrategy.js

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ const domains = config.domains;
55

66
const facebookLogin = async (accessToken, refreshToken, profile, cb) => {
77
try {
8-
console.log('facebookLogin => profile', profile);
9-
const email = profile.emails[0].value;
8+
const email = profile.emails[0]?.value;
109
const facebookId = profile.id;
1110
const oldUser = await User.findOne({
1211
email,
@@ -15,17 +14,17 @@ const facebookLogin = async (accessToken, refreshToken, profile, cb) => {
1514
process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true';
1615

1716
if (oldUser) {
18-
oldUser.avatar = profile.photos[0].value;
17+
oldUser.avatar = profile.photo;
1918
await oldUser.save();
2019
return cb(null, oldUser);
2120
} else if (ALLOW_SOCIAL_REGISTRATION) {
2221
const newUser = await new User({
2322
provider: 'facebook',
2423
facebookId,
25-
username: profile.name.givenName + profile.name.familyName,
24+
username: profile.displayName,
2625
email,
27-
name: profile.displayName,
28-
avatar: profile.photos[0].value,
26+
name: profile.name?.givenName + ' ' + profile.name?.familyName,
27+
avatar: profile.photos[0]?.value,
2928
}).save();
3029

3130
return cb(null, newUser);
@@ -43,23 +42,12 @@ const facebookLogin = async (accessToken, refreshToken, profile, cb) => {
4342
module.exports = () =>
4443
new FacebookStrategy(
4544
{
46-
clientID: process.env.FACEBOOK_APP_ID,
47-
clientSecret: process.env.FACEBOOK_SECRET,
45+
clientID: process.env.FACEBOOK_CLIENT_ID,
46+
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
4847
callbackURL: `${domains.server}${process.env.FACEBOOK_CALLBACK_URL}`,
4948
proxy: true,
50-
// profileFields: [
51-
// 'id',
52-
// 'email',
53-
// 'gender',
54-
// 'profileUrl',
55-
// 'displayName',
56-
// 'locale',
57-
// 'name',
58-
// 'timezone',
59-
// 'updated_time',
60-
// 'verified',
61-
// 'picture.type(large)'
62-
// ]
49+
scope: ['public_profile'],
50+
profileFields: ['id', 'email', 'name'],
6351
},
6452
facebookLogin,
6553
);

client/src/components/Auth/Login.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAuthContext } from '~/hooks/AuthContext';
44
import { useNavigate } from 'react-router-dom';
55
import { useLocalize } from '~/hooks';
66
import { useGetStartupConfig } from 'librechat-data-provider';
7-
import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
7+
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
88

99
function Login() {
1010
const { login, error, isAuthenticated } = useAuthContext();
@@ -65,6 +65,20 @@ function Login() {
6565
</div>
6666
</>
6767
)}
68+
{startupConfig?.facebookLoginEnabled && startupConfig?.socialLoginEnabled && (
69+
<>
70+
<div className="mt-2 flex gap-x-2">
71+
<a
72+
aria-label="Login with Facebook"
73+
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
74+
href={`${startupConfig.serverDomain}/oauth/facebook`}
75+
>
76+
<FacebookIcon />
77+
<p>{localize('com_auth_facebook_login')}</p>
78+
</a>
79+
</div>
80+
</>
81+
)}
6882
{startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && (
6983
<>
7084
<div className="mt-2 flex gap-x-2">

client/src/components/Auth/Registration.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
TRegisterUser,
88
useGetStartupConfig,
99
} from 'librechat-data-provider';
10-
import { GoogleIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
10+
import { GoogleIcon, FacebookIcon, OpenIDIcon, GithubIcon, DiscordIcon } from '~/components';
1111

1212
function Registration() {
1313
const navigate = useNavigate();
@@ -308,6 +308,20 @@ function Registration() {
308308
</div>
309309
</>
310310
)}
311+
{startupConfig?.facebookLoginEnabled && startupConfig?.socialLoginEnabled && (
312+
<>
313+
<div className="mt-2 flex gap-x-2">
314+
<a
315+
aria-label="Login with Facebook"
316+
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
317+
href={`${startupConfig.serverDomain}/oauth/facebook`}
318+
>
319+
<FacebookIcon />
320+
<p>{localize('com_auth_facebook_login')}</p>
321+
</a>
322+
</div>
323+
</>
324+
)}
311325
{startupConfig?.openidLoginEnabled && startupConfig?.socialLoginEnabled && (
312326
<>
313327
<div className="mt-2 flex gap-x-2">

client/src/components/Auth/__tests__/Login.spec.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const setup = ({
2323
isError: false,
2424
data: {
2525
googleLoginEnabled: true,
26+
facebookLoginEnabled: true,
2627
openidLoginEnabled: true,
2728
openidLabel: 'Test OpenID',
2829
openidImageUrl: 'http://test-server.com',
@@ -67,6 +68,21 @@ test('renders login form', () => {
6768
'href',
6869
'mock-server/oauth/google',
6970
);
71+
expect(getByRole('link', { name: /Login with Facebook/i })).toBeInTheDocument();
72+
expect(getByRole('link', { name: /Login with Facebook/i })).toHaveAttribute(
73+
'href',
74+
'mock-server/oauth/facebook',
75+
);
76+
expect(getByRole('link', { name: /Login with Github/i })).toBeInTheDocument();
77+
expect(getByRole('link', { name: /Login with Github/i })).toHaveAttribute(
78+
'href',
79+
'mock-server/oauth/github',
80+
);
81+
expect(getByRole('link', { name: /Login with Discord/i })).toBeInTheDocument();
82+
expect(getByRole('link', { name: /Login with Discord/i })).toHaveAttribute(
83+
'href',
84+
'mock-server/oauth/discord',
85+
);
7086
});
7187

7288
test('calls loginUser.mutate on login', async () => {

client/src/components/Auth/__tests__/Registration.spec.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const setup = ({
2424
isError: false,
2525
data: {
2626
googleLoginEnabled: true,
27+
facebookLoginEnabled: true,
2728
openidLoginEnabled: true,
2829
openidLabel: 'Test OpenID',
2930
openidImageUrl: 'http://test-server.com',
@@ -75,6 +76,21 @@ test('renders registration form', () => {
7576
'href',
7677
'mock-server/oauth/google',
7778
);
79+
expect(getByRole('link', { name: /Login with Facebook/i })).toBeInTheDocument();
80+
expect(getByRole('link', { name: /Login with Facebook/i })).toHaveAttribute(
81+
'href',
82+
'mock-server/oauth/facebook',
83+
);
84+
expect(getByRole('link', { name: /Login with Github/i })).toBeInTheDocument();
85+
expect(getByRole('link', { name: /Login with Github/i })).toHaveAttribute(
86+
'href',
87+
'mock-server/oauth/github',
88+
);
89+
expect(getByRole('link', { name: /Login with Discord/i })).toBeInTheDocument();
90+
expect(getByRole('link', { name: /Login with Discord/i })).toHaveAttribute(
91+
'href',
92+
'mock-server/oauth/discord',
93+
);
7894
});
7995

8096
// eslint-disable-next-line jest/no-commented-out-tests

0 commit comments

Comments
 (0)