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

[ORT-3] feat: add api and auth base code #3

Merged
merged 22 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
302ce95
feat: add auth cookie session storage
aube-dev Aug 16, 2024
1a1578c
refactor: add `AuthSessionStorage` type
aube-dev Aug 16, 2024
4133acc
feat: add api base code
aube-dev Aug 30, 2024
758b41d
refactor: remove unnecessary `if` & treat nullish `serverError`
aube-dev Aug 30, 2024
89779cb
Merge remote-tracking branch 'origin/main' into ORT-3_api
aube-dev Aug 30, 2024
89377ab
build: update `pnpm-lock.yaml`
aube-dev Aug 30, 2024
fb4c64b
feat: use cloudflare env only & modify dev proxy plugin
aube-dev Sep 4, 2024
0030e90
feat: add `.infisical.json`
aube-dev Sep 4, 2024
53ed9a1
feat: add api base code (incomplete)
aube-dev Sep 14, 2024
72395a6
fix: load dev proxy plugin only on dev
aube-dev Sep 14, 2024
364d907
feat: add custom dev proxy vite plugin & move `fetchApi` into `Api` m…
aube-dev Sep 15, 2024
13f8fde
feat: modify api types & add comments
aube-dev Sep 15, 2024
54a5aa8
feat: add `AuthSessionService`
aube-dev Sep 15, 2024
b3c3128
feat: remove unused dependency
aube-dev Sep 15, 2024
38866dd
fix(style): fix wrong comment
aube-dev Sep 15, 2024
875eba9
refactor: rename `ApiReturnType` & fix comments
aube-dev Sep 16, 2024
2467fa2
refactor: simplify `getFetchInfo` logic
aube-dev Sep 16, 2024
6068e0a
feat: add regex to check `API_URL`
aube-dev Sep 16, 2024
bedf770
feat: add sentry & use vite env & add kakao oauth
aube-dev Sep 18, 2024
e1f051e
refactor: remove unnecessary console
aube-dev Sep 19, 2024
fd72176
feat: add `extraErrorDataIntegration`
aube-dev Sep 19, 2024
a749756
fix: remove sourcemap files after upload for safety
aube-dev Sep 19, 2024
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
61 changes: 61 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ module.exports = {
],
format: ['PascalCase'],
},
{
selector: 'typeParameter',
format: ['PascalCase'],
prefix: ['_'],
filter: '^_',
},
{
selector: ['enumMember'],
format: ['UPPER_CASE'],
Expand All @@ -117,6 +123,12 @@ module.exports = {
selector: ['objectLiteralProperty'],
format: null,
},
{
selector: 'variable',
format: ['camelCase'],
prefix: ['api_', 'unstable_'],
filter: '(^api_|^unstable_)',
},
],
},
},
Expand All @@ -128,6 +140,29 @@ module.exports = {
node: true,
},
},

{
files: [
'./server/**/*.{ts,tsx}',
'./vite.config.ts',
'./vite.config.storybook.ts',
],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
// https://github.com/vitejs/vite/issues/10063
group: ['@/*', '@server', 'app/*', 'server/*'],
message:
'server 디렉토리, 또는 Vite config 파일에서는 상대 경로를 사용해 주세요.',
},
],
},
],
},
},
],

rules: {
Expand All @@ -142,5 +177,31 @@ module.exports = {
],
'arrow-body-style': ['warn', 'as-needed'],
'prettier/prettier': 'warn',
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['..*'],
message: '다른 경로에 있는 모듈은 절대 경로로 불러와 주세요.',
},
{
group: ['app/*', 'server/*'],
message: '`@/`, `@server` 등 올바른 절대 경로를 사용해 주세요.',
},
{
group: ['@remix-run/react'],
importNames: ['useFetcher'],
message: '`@/hooks/useTypedFetcher`를 사용해 주세요.',
},
{
group: ['@remix-run/cloudflare'],
importNames: ['redirect'],
message:
'`@/utils/server`의 `redirectWithAuthCookie`를 사용해 주세요.',
},
],
},
],
},
};
11 changes: 9 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ node_modules

/.cache
/build
.env
.wrangler

# macOS
.DS_Store
*storybook.log
*storybook.log

# env
.env
.env.local
.dev.vars

wrangler.toml
5 changes: 5 additions & 0 deletions .infisical.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"workspaceId": "6a0e9b92-038c-4045-beeb-7d9914ef86fb",
"defaultEnvironment": "dev",
"gitBranchToEnvironmentMapping": null
}
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createRemixStub } from '@remix-run/testing';
import type { Preview } from '@storybook/react';
import '../app/root.css';
import '@/root.css';

const preview: Preview = {
parameters: {
Expand Down
21 changes: 21 additions & 0 deletions app/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Api } from '@/models/api';

export const api_loginWithOauth = new Api<
{ oauthKey: string },
{
userId: number;
accessToken: string;
accessTokenExpiresAt: string;
refreshToken: string;
refreshTokenExpiresAt: string;
}
>({
method: 'GET',
endpoint: '/oauth2/token',
needToLogin: false,
request: (variables) => ({
queryParams: {
oauthKey: variables.oauthKey,
},
}),
});
24 changes: 5 additions & 19 deletions app/components/KakaoLoginButton/KakaoLoginButton.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import React, { useMemo } from 'react';
import { getUUID } from '@/utils/random';

const clientId = 'f5aa2f20e42d783654b8e8c01bfc6312';
//redirectUri는 등록된 redirectUri중에 임의로 사용했습니다.
const redirectUri = 'http://localhost:5173/oauth/kakao';

const KakaoLoginButton: React.FC = () => {
const kakaoAuthUrl = useMemo(() => {
const userUUID = getUUID();
return `https://kauth.kakao.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${userUUID}`;
}, []);

return (
<a href={kakaoAuthUrl}>
<button>카카오로 로그인</button>
</a>
);
};
const KakaoLoginButton: React.FC = () => (
<a href="/oauth/kakao">
<button>카카오로 로그인</button>
</a>
);

export default KakaoLoginButton;
11 changes: 11 additions & 0 deletions app/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext } from 'react';

export interface AuthStore {
isLoggedIn: boolean;
}

const AuthContext = createContext<AuthStore>({
isLoggedIn: false,
});

export default AuthContext;
38 changes: 38 additions & 0 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RemixBrowser, useLocation, useMatches } from '@remix-run/react';
import * as Sentry from '@sentry/remix';
import { startTransition, StrictMode, useEffect } from 'react';
import { hydrateRoot } from 'react-dom/client';

Sentry.init({
dsn: import.meta.env.SHARED_SENTRY_DSN,
tracesSampleRate: 1,

integrations: [
Sentry.browserTracingIntegration({
useEffect,
useLocation,
useMatches,
}),
// https://github.com/import-js/eslint-plugin-import/issues/2969#issuecomment-1967510143
// eslint-disable-next-line import/namespace
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
Sentry.extraErrorDataIntegration(),
],

replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1,
environment: import.meta.env.SHARED_APP_MODE,
debug: import.meta.env.SHARED_APP_MODE === 'development',
});

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});
90 changes: 90 additions & 0 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type {
AppLoadContext,
EntryContext,
HandleDataRequestFunction,
} from '@remix-run/cloudflare';
import { RemixServer } from '@remix-run/react';
import * as Sentry from '@sentry/remix';
import * as isbotModule from 'isbot';
import { renderToReadableStream } from 'react-dom/server';

Sentry.init({
dsn: import.meta.env.SHARED_SENTRY_DSN,
tracesSampleRate: 1,
autoInstrumentRemix: true,
environment: import.meta.env.SHARED_APP_MODE,
debug: import.meta.env.SHARED_APP_MODE === 'development',
integrations: [Sentry.extraErrorDataIntegration()],
});

export const handleError = Sentry.sentryHandleError;

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext,
) {
const body = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
// Log streaming rendering errors from inside the shell
console.error(error);
responseStatusCode = 500;
},
},
);

if (isBotRequest(request.headers.get('user-agent'))) {
await body.allReady;
}

responseHeaders.set('Content-Type', 'text/html');

const cookieHeader = await loadContext.authSessionService.commitSession();
if (cookieHeader) {
responseHeaders.append('Set-Cookie', cookieHeader);
}

const response = new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});

return response;
}

export const handleDataRequest: HandleDataRequestFunction = async (
response,
{ context },
) => {
const cookieHeader = await context.authSessionService.commitSession();
if (cookieHeader) {
response.headers.append('Set-Cookie', cookieHeader);
}
return response;
};

// We have some Remix apps in the wild already running with isbot@3 so we need
// to maintain backwards compatibility even though we want new apps to use
// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev.
function isBotRequest(userAgent: string | null) {
if (!userAgent) {
return false;
}

// isbot >= 3.8.0, >4
if ('isbot' in isbotModule && typeof isbotModule.isbot === 'function') {
return isbotModule.isbot(userAgent);
}

// isbot < 3.8.0
if ('default' in isbotModule && typeof isbotModule.default === 'function') {
return isbotModule.default(userAgent);
}

return false;
}
10 changes: 10 additions & 0 deletions app/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useContext } from 'react';
import AuthContext from '@/contexts/AuthContext';

const useAuth = () => {
const authStore = useContext(AuthContext);

return authStore;
};

export default useAuth;
16 changes: 16 additions & 0 deletions app/hooks/useErrorToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { create } from 'zustand';
import type { JsonValue, FrontendErrorResponse } from '@/types/api';

interface ErrorToastStore {
error: FrontendErrorResponse<JsonValue> | null;
setError: (newError: FrontendErrorResponse<JsonValue>) => void;
clearError: () => void;
}

const useErrorToast = create<ErrorToastStore>()((set) => ({
error: null,
setError: (newError) => set({ error: newError }),
clearError: () => set({ error: null }),
}));

export default useErrorToast;
48 changes: 48 additions & 0 deletions app/hooks/useTypedFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { type TypedResponse } from '@remix-run/cloudflare';
// eslint-disable-next-line no-restricted-imports
import { useFetcher } from '@remix-run/react';
import { useEffect } from 'react';
import useErrorToast from './useErrorToast';
import {
type FrontendErrorResponse,
type FrontendSuccessResponse,
type JsonValue,
} from '@/types/api';

/**
* `@remix-run/react`의 `useFetcher` wrapper
* - `FrontendSuccessResponse` 또는 `FrontendErrorResponse` 형태를 반환하는 `action`만 허용
* - 에러 발생 시 에러 토스트 띄우는 로직 적용
* @example
* ```
* const Component = () => {
* const fetcher = useTypedFetcher<typeof action>();
* // ...
* };
* ```
*/
const useTypedFetcher = <
T extends (
...params: unknown[]
) => Promise<
| TypedResponse<FrontendSuccessResponse<JsonValue>>
| TypedResponse<FrontendErrorResponse<JsonValue>>
>,
>(
...params: Parameters<typeof useFetcher<T>>
) => {
const fetcher = useFetcher<T>(...params);
const { setError } = useErrorToast();

useEffect(() => {
if (fetcher.data !== undefined && !fetcher.data.isSuccess) {
setError(fetcher.data);
}
// `fetcher.data` 외 다른 것이 변할 때는 실행되면 안 됨
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher.data]);

return fetcher;
};

export default useTypedFetcher;
Loading