Skip to content

Commit

Permalink
feat: link github account - fix #245 (#281)
Browse files Browse the repository at this point in the history
  • Loading branch information
pagoru authored Jan 27, 2025
1 parent b84631c commit f62df5d
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 13 deletions.
11 changes: 7 additions & 4 deletions app/client/src/modules/account/account.component.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { useUser } from "shared/hooks";
import React from "react";
import { getCensoredEmail } from "shared/utils";
import { LanguagesComponent } from "./components";
import { GithubComponent, LanguagesComponent } from "./components";
//@ts-ignore
import styles from "./account.module.scss";

export const AccountComponent = () => {
const { user } = useUser();

if (!user) return <div>loading...</div>;

return (
<div>
<div className={styles.content}>
<h2>Account</h2>
<p title={user?.email}>{getCensoredEmail(user?.email)}</p>
<p title={user?.accountId}>{user?.username}</p>
<label title={user?.email}>{getCensoredEmail(user?.email)}</label>
<label title={user?.accountId}>{user?.username}</label>
<LanguagesComponent />
<GithubComponent />
</div>
);
};
5 changes: 5 additions & 0 deletions app/client/src/modules/account/account.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.content {
display: flex;
flex-direction: column;
gap: 1rem;
}
117 changes: 117 additions & 0 deletions app/client/src/modules/account/components/github/github.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useCallback, useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useAccount, useApi, useUser } from "shared/hooks";
import { RequestMethod } from "shared/enums";
import { ButtonComponent, GithubIconComponent } from "@oh/components";
import { RedirectComponent } from "shared/components";

//https://github.com/login/oauth/authorize?client_id=Ov23liP5bbBloeAyvY5h&redirect_uri=http://localhost:2024/account/github&state=akjsdaklsdjkl
export const GithubComponent: React.FC = () => {
const [searchParams] = useSearchParams();
const { getAccountHeaders, isLogged } = useAccount();
const { fetch } = useApi();
const navigate = useNavigate();
const { user } = useUser();

const [code, setCode] = useState(searchParams.get("code"));
const [state, setstate] = useState(searchParams.get("state"));

const [githubLogin, setGithubLogin] = useState(user?.githubLogin);

const [enabled, setEnabled] = useState<boolean>(null);
const [loaded, setLoaded] = useState<boolean>(true);

useEffect(() => {
if (enabled !== null) return;
fetch({
method: RequestMethod.GET,
pathname: "/account/misc/github?enabled=enabled",
headers: getAccountHeaders(),
}).then(({ enabled }) => {
setEnabled(enabled);
});
}, [fetch, getAccountHeaders, setEnabled, enabled]);

useEffect(() => {
if (!user || !user?.githubLogin || !enabled) return;
setGithubLogin(user?.githubLogin);
}, [user, setGithubLogin, enabled]);

useEffect(() => {
if (!isLogged || !user || !searchParams.get("github") || !enabled) return;
setLoaded(false);
const url = new URL(window.location.href);
url.searchParams.delete("github");
url.searchParams.delete("code");
url.searchParams.delete("state");
setCode(null);
setstate(null);

window.history.replaceState({}, "", url);

fetch({
method: RequestMethod.POST,
pathname: "/account/misc/github",
body: {
code,
state,
},
headers: getAccountHeaders(),
}).then(({ login }) => {
setGithubLogin(login);
setLoaded(true);
});
}, [
isLogged,
user,
getAccountHeaders,
setstate,
setCode,
setLoaded,
enabled,
]);

const onClickLinkGithub = useCallback(async () => {
const data = await fetch({
method: RequestMethod.GET,
pathname: "/account/misc/github",
headers: getAccountHeaders(),
});
window.location.href = data.url;
}, [fetch, getAccountHeaders, navigate]);

const onClickUnlinkGithub = useCallback(async () => {
await fetch({
method: RequestMethod.DELETE,
pathname: "/account/misc/github",
headers: getAccountHeaders(),
}).then(() => setGithubLogin(null));
}, [fetch, getAccountHeaders, navigate, setGithubLogin]);

if (!enabled) return null;

if (code || state)
return (
<RedirectComponent
to={`/account?github=link&state=${state}&code=${code}`}
/>
);

return (
<div>
{loaded ? (
githubLogin ? (
<ButtonComponent onClick={onClickUnlinkGithub} color="grey">
<GithubIconComponent /> Unlink '{githubLogin}' Github account
</ButtonComponent>
) : (
<ButtonComponent onClick={onClickLinkGithub}>
<GithubIconComponent /> Link Github account
</ButtonComponent>
)
) : (
<label>loading...</label>
)}
</div>
);
};
1 change: 1 addition & 0 deletions app/client/src/modules/account/components/github/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./github.component";
1 change: 1 addition & 0 deletions app/client/src/modules/account/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./my-hotels";
export * from "./otp";
export * from "./navigator";
export * from "./languages";
export * from "./github";
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
AccountComponent,
BskyComponent,
ConnectionsComponent,
GithubComponent,
MyHotelsComponent,
} from "modules/account";
import {
Expand Down Expand Up @@ -104,6 +105,10 @@ const router = createBrowserRouter([
path: "bsky",
element: <BskyComponent />,
},
{
path: "github",
element: <GithubComponent />,
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ type Props = {

export const WrapperLayoutComponent = ({ children }: Props) => {
const { isLogged } = useAccount();
const { user, initUser } = useUser();
const { user, refresh } = useUser();

useEffect(() => {
if (!isLogged || user) return;
initUser();
}, [user, isLogged, initUser]);
refresh();
}, [user, isLogged, refresh]);

if (isLogged === null) return <div>Loading...</div>;
if (!isLogged) return <RedirectComponent to="/login" />;
Expand Down
10 changes: 5 additions & 5 deletions app/client/src/shared/hooks/useUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type UserState = {
user: User | null;

getLicense: () => Promise<string>;
initUser: () => Promise<void>;
refresh: () => Promise<void>;

update: (data: { languages: string[] }) => void;

Expand Down Expand Up @@ -57,7 +57,7 @@ export const UserProvider: React.FunctionComponent<ProviderProps> = ({
return data.licenseToken;
}, [fetch, getAccountHeaders]);

const initUser = useCallback(
const refresh = useCallback(
() =>
fetchUser()
.then(setUser)
Expand All @@ -73,9 +73,9 @@ export const UserProvider: React.FunctionComponent<ProviderProps> = ({
headers: getAccountHeaders(),
body,
})
.then(initUser)
.then(refresh)
.catch(() => navigate("/login")),
[fetchUser, setUser, navigate, getAccountHeaders],
[fetchUser, navigate, getAccountHeaders],
);

const clear = useCallback(() => {
Expand All @@ -88,7 +88,7 @@ export const UserProvider: React.FunctionComponent<ProviderProps> = ({
user,
getLicense,

initUser,
refresh,

update,

Expand Down
1 change: 1 addition & 0 deletions app/client/src/shared/types/user.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export type User = {
admin?: boolean;
otp?: boolean;
verified?: boolean;
githubLogin?: string;
};
110 changes: 110 additions & 0 deletions app/server/src/modules/api/v3/account/misc/github.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
RequestMethod,
RequestType,
getResponse,
getRandomString,
HttpStatusCode,
} from "@oh/utils";
import { System } from "modules/system/main.ts";
import { hasRequestAccess } from "shared/utils/scope.utils.ts";
import { RequestKind } from "shared/enums/request.enums.ts";

export const githubGetRequest: RequestType = {
method: RequestMethod.GET,
pathname: "/github",
kind: RequestKind.ACCOUNT,
func: async (request: Request, url: URL) => {
if (!(await hasRequestAccess({ request })))
return getResponse(HttpStatusCode.FORBIDDEN);

const config = System.getConfig();

const enabled = url.searchParams.get("enabled");
if (enabled)
return getResponse(HttpStatusCode.OK, {
enabled: config.github.enabled,
});

if (!config.github.enabled) return getResponse(HttpStatusCode.IM_A_TEAPOT);

const account = await System.accounts.getFromRequest(request);
const state = getRandomString(32);

const redirectUri = `${config.url}/account/github`;

const expireIn = 60 * 60 * 1000; /* 1h */
System.db.set(["githubState", account.accountId], state, { expireIn });

return getResponse(HttpStatusCode.OK, {
url: `https://github.com/login/oauth/authorize?client_id=${config.github.clientId}&redirect_uri=${redirectUri}&state=${state}`,
});
},
};

export const githubPostRequest: RequestType = {
method: RequestMethod.POST,
pathname: "/github",
kind: RequestKind.ACCOUNT,
func: async (request: Request) => {
if (!(await hasRequestAccess({ request })))
return getResponse(HttpStatusCode.FORBIDDEN);

const config = System.getConfig();
if (!config.github.enabled) return getResponse(HttpStatusCode.IM_A_TEAPOT);

const { code, state } = await request.json();

const account = await System.accounts.getFromRequest(request);

const foundState = await System.db.get(["githubState", account.accountId]);
if (state !== foundState) return getResponse(HttpStatusCode.FORBIDDEN);

const url = new URL("https://github.com/login/oauth/access_token");
url.searchParams.append("client_id", config.github.clientId);
url.searchParams.append("client_secret", config.github.clientSecret);
url.searchParams.append("code", code);

const tokenResponse = await fetch(url.href, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
});
const { access_token } = await tokenResponse.json();

const userResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
const { login } = await userResponse.json();

await System.db.set(["github", account.accountId], {
login,
});

return getResponse(HttpStatusCode.OK, {
login,
});
},
};

export const githubDeleteRequest: RequestType = {
method: RequestMethod.DELETE,
pathname: "/github",
kind: RequestKind.ACCOUNT,
func: async (request: Request) => {
if (!(await hasRequestAccess({ request })))
return getResponse(HttpStatusCode.FORBIDDEN);

const config = System.getConfig();
if (!config.github.enabled) return getResponse(HttpStatusCode.IM_A_TEAPOT);

const account = await System.accounts.getFromRequest(request);

System.db.delete(["github", account.accountId]);

return getResponse(HttpStatusCode.OK);
},
};
12 changes: 11 additions & 1 deletion app/server/src/modules/api/v3/account/misc/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { RequestType, getPathRequestList } from "@oh/utils";
import { bskyPostRequest } from "./bsky.request.ts";
import {
githubDeleteRequest,
githubGetRequest,
githubPostRequest,
} from "./github.request.ts";

export const miscRequestList: RequestType[] = getPathRequestList({
requestList: [bskyPostRequest],
requestList: [
bskyPostRequest,
githubGetRequest,
githubPostRequest,
githubDeleteRequest,
],
pathname: "/misc",
});
3 changes: 3 additions & 0 deletions app/server/src/modules/api/v3/user/@me/main.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ export const mainGetRequest: RequestType = {
const account = await System.accounts.getFromRequest(request);
const admin = Boolean(await System.admins.get(account.accountId));

const githubData = await System.db.get(["github", account.accountId]);

return getResponse(HttpStatusCode.OK, {
data: {
accountId: account.accountId,
username: account.username,
languages: account.languages,
...(githubData?.login ? { githubLogin: githubData?.login } : {}),
...(admin ? { admin } : {}),
},
});
Expand Down
5 changes: 5 additions & 0 deletions app/server/src/shared/consts/config.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ export const CONFIG_DEFAULT: ConfigTypes = {
username: "",
password: "",
},
github: {
enabled: false,
clientId: "",
clientSecret: "",
},
};
Loading

0 comments on commit f62df5d

Please sign in to comment.