Skip to content

Commit

Permalink
Merge pull request #515 from sparcs-kaist/google
Browse files Browse the repository at this point in the history
Feat/ Google Login
  • Loading branch information
rjsdn0 authored Dec 2, 2024
2 parents c5761f1 + bd6c7e1 commit b1e8fba
Show file tree
Hide file tree
Showing 13 changed files with 8,979 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@socket.io/component-emitter": "^3.1.0",
"cors": "^2.8.5",
"express": "^4.18.2",
"google-auth-library": "^9.15.0",
"jsonwebtoken": "^9.0.0",
"ldapts": "^4.2.6",
"prisma": "^4.16.2",
Expand Down
25 changes: 25 additions & 0 deletions packages/api/src/auth/google.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { z } from "zod";
import { OAuth2Client } from "google-auth-library";
import { env } from "@biseo/api/env";
import jwt from "jsonwebtoken";

const Token = z.object({
email: z.string(),
});

export const gauthenticate = async (code: string): Promise<string | null> => {
try {
const logininfo = new OAuth2Client(
env.GOOGLE_CLIENT,
env.GOOGLE_SECRET,
"postmessage",
);
const token = await logininfo.getToken(code);
const mail = Token.parse(jwt.decode(token.tokens.id_token!)).email;
const username =
mail.split("@")[1] === "sparcs.org" ? mail.split("@")[0] : null;
return username;
} catch {
return null;
}
};
14 changes: 14 additions & 0 deletions packages/api/src/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Request, Response } from "express";
import { prisma } from "@biseo/api/db/prisma";

import { authenticate } from "./ldap";
import { gauthenticate } from "./google";
import { getToken } from "./token";

const Login = z.object({
Expand All @@ -25,3 +26,16 @@ export const loginHandler = async (req: Request, res: Response) => {

return res.json({ token: getToken(user.username) });
};

export const gLoginHandler = async (req: Request, res: Response) => {
const result = req.body;

const username = await gauthenticate(result.code);
if (!username) return res.status(401).send("Unauthorized");

const user =
(await prisma.user.findUnique({ where: { username } })) ||
(await prisma.user.create({ data: { username, displayName: username } }));

return res.json({ token: getToken(user.username) });
};
5 changes: 4 additions & 1 deletion packages/api/src/auth/router.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Router } from "express";
import { loginHandler } from "./login";
import { loginHandler, gLoginHandler } from "./login";

const router = Router();

router.post("/login", (req, res, next) => {
loginHandler(req, res).catch(next);
});
router.post("/glogin", (req, res, next) => {
gLoginHandler(req, res).catch(next);
});

export { router as authRouter };
2 changes: 2 additions & 0 deletions packages/api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const schema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
SERVER_PORT: z.coerce.number(),
SECRET_KEY: z.string(),
GOOGLE_CLIENT: z.string(),
GOOGLE_SECRET: z.string(),
});

export const env = schema.parse(process.env);
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@react-oauth/google": "^0.12.1",
"axios": "^1.4.0",
"framer-motion": "^10.16.1",
"immer": "^10.0.2",
Expand Down
Binary file added packages/web/src/assets/google.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions packages/web/src/common/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ export const getToken = async (username: string, password: string) => {
return null;
}
};

export const getGoogleToken = async (code: string) => {
try {
const res = await axios.post<{ token: string }>(
`${API_BASE}/api/auth/glogin`,
{
code,
},
);
return res.data.token;
} catch {
return null;
}
};
35 changes: 31 additions & 4 deletions packages/web/src/components/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { motion } from "framer-motion";

import { LogoLargeIcon } from "@biseo/web/assets";
import LandingImg from "@biseo/web/assets/landing.png";
import GoogleLogo from "@biseo/web/assets/google.png";
import { useAuth } from "@biseo/web/services/auth";

import { useInput } from "@biseo/web/common/hooks";
import { theme } from "@biseo/web/theme";
import { Box, Text } from "@biseo/web/components/atoms";
import { text } from "@biseo/web/styles";
import { useGoogleLogin } from "@react-oauth/google";

const Page = styled.div`
width: 100vw;
Expand Down Expand Up @@ -63,7 +65,7 @@ const InputContainer = styled.input`

const LoginButton = styled.button`
width: 320px;
height: 45px;
height: 48px;
background-color: ${theme.colors.blue200};
border: none;
Expand All @@ -78,6 +80,7 @@ const LoginButton = styled.button`
flex-direction: row;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s;
Expand All @@ -86,6 +89,9 @@ const LoginButton = styled.button`
const LoginBackground = styled.img`
width: 100%;
max-width: 1700px;
height: 30%;
object-fit: cover;
position: absolute;
opacity: 30%;
Expand All @@ -95,8 +101,9 @@ const LoginBackground = styled.img`
`;

export const LoginPage: React.FC = () => {
const { login, isLoggedIn } = useAuth(state => ({
const { login, glogin, isLoggedIn } = useAuth(state => ({
login: state.login,
glogin: state.glogin,
isLoggedIn: !!state.userInfo,
}));
const [error, setError] = useState<boolean>(false);
Expand Down Expand Up @@ -124,6 +131,21 @@ export const LoginPage: React.FC = () => {
},
[username.value, password.value],
);
const handleGLogin = useCallback((code: string) => {
glogin(code)
.then(() => console.log("Login success!"))

Check warning on line 136 in packages/web/src/components/pages/LoginPage.tsx

View workflow job for this annotation

GitHub Actions / Lint and Format (18.x, 8.x)

Unexpected console statement
.catch(() => setError(true));
}, []);
const googleLogin = useGoogleLogin({
scope: "email",
onSuccess: async ({ code }) => {
handleGLogin(code);
},
onError: errorResponse => {
console.error(errorResponse);

Check warning on line 145 in packages/web/src/components/pages/LoginPage.tsx

View workflow job for this annotation

GitHub Actions / Lint and Format (18.x, 8.x)

Unexpected console statement
},
flow: "auth-code",
});

if (isLoggedIn) return <Navigate to="/" replace />;

Expand All @@ -139,7 +161,6 @@ export const LoginPage: React.FC = () => {
쉽고 빠른 의사결정은, Biseo
</LoginTitle>
</Box>

<form onSubmit={handleLogin}>
<Box dir="column" gap={12} align="center">
<InputContainer
Expand All @@ -162,13 +183,19 @@ export const LoginPage: React.FC = () => {
)}

<LoginButton>
<Text variant="body" color="blue600">
<Text variant="boldtitle2" color="blue600">
로그인
</Text>
</LoginButton>
</Box>
</Box>
</form>
<LoginButton onClick={googleLogin}>
<img alt="google-logo" style={{ width: "20px" }} src={GoogleLogo} />
<Text variant="boldtitle2" color="blue600">
구글 로그인
</Text>
</LoginButton>
<LoginBackground src={LandingImg} />
</Page>
);
Expand Down
13 changes: 8 additions & 5 deletions packages/web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { ThemeProvider } from "@emotion/react";
import { enableMapSet } from "immer";
import { GoogleOAuthProvider } from "@react-oauth/google";

import router from "@biseo/web/router";
import { theme } from "@biseo/web/theme";
Expand All @@ -12,9 +13,11 @@ import "./index.css";
enableMapSet();

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>,
<GoogleOAuthProvider clientId="630761439137-ouv559b1su1h52t0jqau3g41ok2cf3p0.apps.googleusercontent.com">
<React.StrictMode>
<ThemeProvider theme={theme}>
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>
</GoogleOAuthProvider>,
);
11 changes: 10 additions & 1 deletion packages/web/src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import type { Init } from "@biseo/interface/init";

import { socket } from "@biseo/web/socket";
import { initSocket } from "@biseo/web/socket/init";
import { getToken } from "@biseo/web/common/api/auth";
import { getToken, getGoogleToken } from "@biseo/web/common/api/auth";

interface AuthState {
token: string | null;
userInfo: Init | null;
init: () => Promise<Init | null>;
login: (username: string, password: string) => Promise<void>;
glogin: (code: string) => Promise<void>;
logout: () => void;
}

Expand Down Expand Up @@ -42,6 +43,14 @@ const useAuth = create<AuthState>()(
const userInfo = await initSocket(token);
set({ token, userInfo });
},
glogin: async code => {
const token = await getGoogleToken(code);

if (!token) throw new Error("incorrect username");

const userInfo = await initSocket(token);
set({ token, userInfo });
},
logout: () => {
set({ token: null, userInfo: null });
socket.disconnect();
Expand Down
Loading

0 comments on commit b1e8fba

Please sign in to comment.