Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Login by social providers (Google for now) #1326

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,10 @@ GID='1000'
# AGENT_SERPER_DEV_KEY=

#------ Bing Search ----------- https://portal.azure.com/
# AGENT_BING_SEARCH_API_KEY=
# AGENT_BING_SEARCH_API_KEY=

###########################################
######## SOCIAL PROVIDERS KEYS ###############
###########################################

# GOOGLE_AUTH_CLIENT_ID=
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@metamask/jazzicon": "^2.0.0",
"@microsoft/fetch-event-source": "^2.0.1",
"@phosphor-icons/react": "^2.0.13",
"@react-oauth/google": "^0.12.1",
"@tremor/react": "^3.15.1",
"dompurify": "^3.0.8",
"file-saver": "^2.0.5",
Expand Down
21 changes: 17 additions & 4 deletions frontend/src/AuthContext.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { useState, createContext } from "react";
import React, { useState, createContext, useEffect } from "react";
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
import { GoogleOAuthProvider } from "@react-oauth/google";
import System from "./models/system";

export const AuthContext = createContext(null);
export function ContextWrapper(props) {
const localUser = localStorage.getItem(AUTH_USER);
const localAuthToken = localStorage.getItem(AUTH_TOKEN);
const [googleAuthClientId, setGoogleAuthClientId] = useState(null);
const [store, setStore] = useState({
user: localUser ? JSON.parse(localUser) : null,
authToken: localAuthToken ? localAuthToken : null,
Expand All @@ -24,9 +27,19 @@ export function ContextWrapper(props) {
},
});

useEffect(() => {
async function fetchSettings() {
const _settings = await System.keys();
setGoogleAuthClientId(_settings?.GoogleAuthClientId);
}
fetchSettings();
}, []);

return (
<AuthContext.Provider value={{ store, actions }}>
{props.children}
</AuthContext.Provider>
<GoogleOAuthProvider clientId={googleAuthClientId}>
<AuthContext.Provider value={{ store, actions }}>
{props.children}
</AuthContext.Provider>
</GoogleOAuthProvider>
);
}
9 changes: 8 additions & 1 deletion frontend/src/components/Modals/Password/MultiUserAuth.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import showToast from "@/utils/toast";
import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal";
import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal";
import SocialProviders from "./SocialProviders";

const RecoveryForm = ({ onSubmit, setShowRecoveryForm }) => {
const [username, setUsername] = useState("");
Expand Down Expand Up @@ -275,7 +276,7 @@ export default function MultiUserAuth() {
<>
<form onSubmit={handleLogin}>
<div className="flex flex-col justify-center items-center relative rounded-2xl md:bg-login-gradient md:shadow-[0_4px_14px_rgba(0,0,0,0.25)] md:px-12 py-12 -mt-4 md:mt-0">
<div className="flex items-start justify-between pt-11 pb-9 rounded-t">
<div className="flex items-start justify-between pt-32 pb-4 2xl:pt-11 rounded-t">
<div className="flex items-center flex-col gap-y-4">
<div className="flex gap-x-1">
<h3 className="text-md md:text-2xl font-bold text-white text-center white-space-nowrap hidden md:block">
Expand All @@ -290,6 +291,12 @@ export default function MultiUserAuth() {
</p>
</div>
</div>
<SocialProviders
setError={setError}
setLoading={setLoading}
setUser={setUser}
setToken={setToken}
/>
<div className="w-full px-4 md:px-12">
<div className="w-full flex flex-col gap-y-4">
<div className="w-screen md:w-full md:px-0 px-6">
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/components/Modals/Password/SocialProviders.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import System from "@/models/system";
import { AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
import paths from "@/utils/paths";
import { GoogleLogin } from "@react-oauth/google";
import { useEffect, useState } from "react";

export default function SocialProviders({
setError,
setLoading,
setUser,
setToken,
}) {
const [settings, setSettings] = useState(null);

useEffect(() => {
async function fetchSettings() {
const _settings = await System.keys();
setSettings(_settings);
}
fetchSettings();
}, []);

const handleGoogleLogin = async (data) => {
setError(null);
setLoading(true);
const { valid, user, token, message } = await System.socialLogin(
data,
"google"
);
if (valid && !!token && !!user) {
setUser(user);
setToken(token);

window.localStorage.setItem(AUTH_USER, JSON.stringify(user));
window.localStorage.setItem(AUTH_TOKEN, token);
window.location = paths.home();
} else {
setError(message);
setLoading(false);
}
setLoading(false);
};

return (
<>
{settings?.GoogleAuthClientId && (
<>
<GoogleLogin
onSuccess={(credentialResponse) => {
handleGoogleLogin(credentialResponse);
}}
onError={() => {
setError("Something went wrong");
}}
theme={"filled_black"}
/>
{/* Add here other social providers */}
<p className="text-sm text-white/90 text-center my-2">or</p>
</>
)}
</>
);
}
14 changes: 14 additions & 0 deletions frontend/src/models/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ const System = {
return { valid: false, message: e.message };
});
},
socialLogin: async function (body, provider) {
return await fetch(`${API_BASE}/social-login/${provider}`, {
method: "POST",
body: JSON.stringify({ ...body }),
})
.then((res) => {
if (!res.ok) throw new Error("Could not validate login.");
return res.json();
})
.then((res) => res)
.catch((e) => {
return { valid: false, message: e.message };
});
},
recoverAccount: async function (username, recoveryCodes) {
return await fetch(`${API_BASE}/system/recover-account`, {
method: "POST",
Expand Down
100 changes: 100 additions & 0 deletions frontend/src/pages/Admin/System/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isMobile } from "react-device-detect";
import Admin from "@/models/admin";
import showToast from "@/utils/toast";
import CTAButton from "@/components/lib/CTAButton";
import System from "@/models/system";

export default function AdminSystem() {
const [saving, setSaving] = useState(false);
Expand All @@ -13,6 +14,11 @@ export default function AdminSystem() {
enabled: false,
limit: 10,
});
const [canLoginWithGoogle, setCanLoginWithGoogle] = useState({
enabled: false,
clientId: null,
allowedDomain: null,
});

const handleSubmit = async (e) => {
e.preventDefault();
Expand All @@ -21,7 +27,14 @@ export default function AdminSystem() {
users_can_delete_workspaces: canDelete,
limit_user_messages: messageLimit.enabled,
message_limit: messageLimit.limit,
users_can_login_with_google: canLoginWithGoogle.enabled,
allowed_domain: canLoginWithGoogle.allowedDomain,
});
if (canLoginWithGoogle.enabled && canLoginWithGoogle.clientId) {
await System.updateSystem({
GoogleAuthClientId: canLoginWithGoogle.clientId,
});
}
setSaving(false);
setHasChanges(false);
showToast("System preferences updated successfully.", "success");
Expand All @@ -36,6 +49,12 @@ export default function AdminSystem() {
enabled: settings.limit_user_messages,
limit: settings.message_limit,
});
setCanLoginWithGoogle({
...canLoginWithGoogle,
enabled: settings.users_can_login_with_google,
clientId: settings.users_can_login_with_google ? "*".repeat(20) : "",
allowedDomain: settings.allowed_domain,
});
}
fetchSettings();
}, []);
Expand Down Expand Up @@ -148,6 +167,87 @@ export default function AdminSystem() {
</div>
)}
</div>

<div className="mb-8">
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Users can login with Google
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Enable this option if you want users to be able to log in using
their Google accounts. You can restrict access to users with
emails from your organization's domain.
</p>
<div className="mt-2">
<label className="relative inline-flex cursor-pointer items-center">
<input
type="checkbox"
name="users_can_login_with_google"
checked={canLoginWithGoogle.enabled}
onChange={(e) => {
setCanLoginWithGoogle({
...canLoginWithGoogle,
enabled: e.target.checked,
});
}}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
</div>
</div>
{canLoginWithGoogle.enabled && (
<div className="mt-4">
<label className="block text-sm font-medium text-white">
Client ID
</label>
<div className="relative mt-2">
<input
type="password"
name="google_client_id"
onScroll={(e) => e.target.blur()}
onChange={(e) => {
setCanLoginWithGoogle({
...canLoginWithGoogle,
clientId: e.target.value,
});
}}
value={canLoginWithGoogle.clientId}
min={1}
max={300}
className="w-1/3 rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-slate-200 dark:text-slate-200 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
</div>

<label className="block text-sm font-medium text-white mt-2">
Organization domain
</label>
<p className="text-xs leading-[18px] font-base text-white/60">
Restrict access to a specific domain, or leave empty to allow
login with any Google account.
</p>
<div className="relative mt-2">
<input
type="text"
placeholder="example.com"
name="allowed_domain"
onScroll={(e) => e.target.blur()}
onChange={(e) => {
setCanLoginWithGoogle({
...canLoginWithGoogle,
allowedDomain: e.target.value,
});
}}
value={canLoginWithGoogle.allowedDomain}
min={1}
max={300}
className="w-1/3 rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-slate-200 dark:text-slate-200 outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary"
/>
</div>
</div>
)}
</div>
</form>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,11 @@
picocolors "^1.0.0"
tslib "^2.6.0"

"@react-oauth/google@^0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@react-oauth/google/-/google-0.12.1.tgz#b76432c3a525e9afe076f787d2ded003fcc1bee9"
integrity sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==

"@remix-run/[email protected]":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.14.1.tgz#6d2dd03d52e604279c38911afc1079d58c50a755"
Expand Down
6 changes: 6 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,9 @@ TTS_PROVIDER="native"

#------ Bing Search ----------- https://portal.azure.com/
# AGENT_BING_SEARCH_API_KEY=

###########################################
######## SOCIAL PROVIDERS KEYS ###############
###########################################

# GOOGLE_AUTH_CLIENT_ID=
6 changes: 6 additions & 0 deletions server/endpoints/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,12 @@ function adminEndpoints(app) {
?.value,
[]
) || [],
users_can_login_with_google:
(await SystemSettings.get({ label: "users_can_login_with_google" }))
?.value === "true",
allowed_domain: (
await SystemSettings.get({ label: "allowed_domain" })
)?.value,
custom_app_name:
(await SystemSettings.get({ label: "custom_app_name" }))?.value ||
null,
Expand Down
Loading