Skip to content

Commit

Permalink
Merge branch 'master' into fix/improve-react-code
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-dalmet authored Oct 21, 2024
2 parents 54824f0 + 2bb986d commit 4d15427
Show file tree
Hide file tree
Showing 23 changed files with 949 additions and 11 deletions.
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ NEXT_PUBLIC_IS_DEMO="false"
# DATABASE
DATABASE_URL="postgres://${DOCKER_DATABASE_USERNAME}:${DOCKER_DATABASE_PASSWORD}@localhost:${DOCKER_DATABASE_PORT}/${DOCKER_DATABASE_NAME}"

# GITHUB
GITHUB_CLIENT_ID="REPLACE ME"
GITHUB_CLIENT_SECRET="REPLACE ME"

# GOOGLE
GOOGLE_CLIENT_ID="REPLACE ME"
GOOGLE_CLIENT_SECRET="REPLACE ME"

# DISCORD
DISCORD_CLIENT_ID="REPLACE ME"
DISCORD_CLIENT_SECRET="REPLACE ME"

# EMAILS
EMAIL_SERVER="smtp://username:password@0.0.0.0:1025"
EMAIL_FROM="Start UI <noreply@example.com>"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@trpc/client": "10.45.2",
"@trpc/react-query": "10.45.2",
"@trpc/server": "10.45.2",
"arctic": "1.9.2",
"bcrypt": "5.1.1",
"chakra-react-select": "4.9.1",
"colorette": "2.0.20",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions prisma/schema/auth.prisma
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
model OAuthAccount {
provider String
providerUserId String
userId String
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
@@id([provider, providerUserId])
}

model Session {
id String @id
userId String
Expand Down
17 changes: 9 additions & 8 deletions prisma/schema/user.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ enum UserRole {
}

model User {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
email String? @unique
isEmailVerified Boolean @default(false)
accountStatus AccountStatus @default(NOT_VERIFIED)
email String? @unique
isEmailVerified Boolean @default(false)
accountStatus AccountStatus @default(NOT_VERIFIED)
image String?
authorizations UserRole[] @default([APP])
language String @default("en")
authorizations UserRole[] @default([APP])
language String @default("en")
lastLoginAt DateTime?
session Session[]
oauth OAuthAccount[]
}
13 changes: 13 additions & 0 deletions src/app/oauth/[provider]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';

import { Suspense } from 'react';

import PageOAuthCallback from '@/features/auth/PageOAuthCallback';

export default function Page() {
return (
<Suspense>
<PageOAuthCallback />
</Suspense>
);
}
40 changes: 37 additions & 3 deletions src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

const zNodeEnv = () =>
z.enum(['development', 'test', 'production']).default('development');

export const env = createEnv({
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
Expand All @@ -15,6 +12,15 @@ export const env = createEnv({
DATABASE_URL: z.string().url(),
NODE_ENV: zNodeEnv(),

GITHUB_CLIENT_ID: zOptionalWithReplaceMe(),
GITHUB_CLIENT_SECRET: zOptionalWithReplaceMe(),

GOOGLE_CLIENT_ID: zOptionalWithReplaceMe(),
GOOGLE_CLIENT_SECRET: zOptionalWithReplaceMe(),

DISCORD_CLIENT_ID: zOptionalWithReplaceMe(),
DISCORD_CLIENT_SECRET: zOptionalWithReplaceMe(),

EMAIL_SERVER: z.string().url(),
EMAIL_FROM: z.string(),
LOGGER_LEVEL: z
Expand Down Expand Up @@ -77,6 +83,15 @@ export const env = createEnv({
LOGGER_LEVEL: process.env.LOGGER_LEVEL,
LOGGER_PRETTY: process.env.LOGGER_PRETTY,

GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,

GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,

DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,

NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: process.env.NEXT_PUBLIC_BASE_URL,
Expand All @@ -92,3 +107,22 @@ export const env = createEnv({
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
});

function zNodeEnv() {
return z.enum(['development', 'test', 'production']).default('development');
}

function zOptionalWithReplaceMe() {
return z
.string()
.optional()
.refine(
(value) =>
// Check in prodution if the value is not REPLACE ME
process.env.NODE_ENV !== 'production' || value !== 'REPLACE ME',
{
message: 'Update the value "REPLACE ME" or remove the variable',
}
)
.transform((value) => (value === 'REPLACE ME' ? undefined : value));
}
90 changes: 90 additions & 0 deletions src/features/auth/OAuthLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
Button,
ButtonProps,
Divider,
Flex,
SimpleGrid,
Text,
} from '@chakra-ui/react';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';

import { Icon } from '@/components/Icons';
import { toastCustom } from '@/components/Toast';
import {
OAUTH_PROVIDERS,
OAUTH_PROVIDERS_ENABLED_ARRAY,
OAuthProvider,
} from '@/features/auth/oauth-config';
import { trpc } from '@/lib/trpc/client';

export const OAuthLoginButton = ({
provider,
...rest
}: {
provider: OAuthProvider;
} & ButtonProps) => {
const { t } = useTranslation(['auth']);
const router = useRouter();
const loginWith = trpc.oauth.createAuthorizationUrl.useMutation({
onSuccess: (data) => {
router.push(data.url);
},
onError: (error) => {
toastCustom({
status: 'error',
title: t('auth:login.feedbacks.oAuthError.title', {
provider: OAUTH_PROVIDERS[provider].label,
}),
description: error.message,
});
},
});

return (
<Button
onClick={() => loginWith.mutate({ provider: provider })}
isLoading={loginWith.isLoading || loginWith.isSuccess}
leftIcon={<Icon icon={OAUTH_PROVIDERS[provider].icon} />}
{...rest}
>
{OAUTH_PROVIDERS[provider].label}
</Button>
);
};

export const OAuthLoginButtonsGrid = () => {
if (!OAUTH_PROVIDERS_ENABLED_ARRAY.length) return null;
return (
<SimpleGrid columns={2} gap={3}>
{OAUTH_PROVIDERS_ENABLED_ARRAY.map(({ provider }) => {
return (
<OAuthLoginButton
key={provider}
provider={provider}
_first={{
gridColumn:
OAUTH_PROVIDERS_ENABLED_ARRAY.length % 2 !== 0
? 'span 2'
: undefined,
}}
/>
);
})}
</SimpleGrid>
);
};

export const OAuthLoginDivider = () => {
const { t } = useTranslation(['common']);
if (!OAUTH_PROVIDERS_ENABLED_ARRAY.length) return null;
return (
<Flex alignItems="center" gap={2}>
<Divider flex={1} />
<Text fontSize="xs" color="text-dimmed" textTransform="uppercase">
{t('common:or')}
</Text>
<Divider flex={1} />
</Flex>
);
};
8 changes: 8 additions & 0 deletions src/features/auth/PageLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';

import { LoginForm } from '@/features/auth/LoginForm';
import {
OAuthLoginButtonsGrid,
OAuthLoginDivider,
} from '@/features/auth/OAuthLogin';
import { ROUTES_AUTH } from '@/features/auth/routes';
import type { RouterInputs, RouterOutputs } from '@/lib/trpc/types';

Expand Down Expand Up @@ -51,6 +55,10 @@ export default function PageLogin() {
</Box>
</Button>
</Stack>

<OAuthLoginButtonsGrid />
<OAuthLoginDivider />

<LoginForm onSuccess={handleOnSuccess} />
</Stack>
);
Expand Down
70 changes: 70 additions & 0 deletions src/features/auth/PageOAuthCallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useEffect, useRef } from 'react';

import {
notFound,
useParams,
useRouter,
useSearchParams,
} from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';

import { LoaderFull } from '@/components/LoaderFull';
import { toastCustom } from '@/components/Toast';
import { ROUTES_ADMIN } from '@/features/admin/routes';
import { ROUTES_APP } from '@/features/app/routes';
import { zOAuthProvider } from '@/features/auth/oauth-config';
import { ROUTES_AUTH } from '@/features/auth/routes';
import { trpc } from '@/lib/trpc/client';

export default function PageOAuthCallback() {
const { i18n, t } = useTranslation(['auth']);
const router = useRouter();
const isTriggeredRef = useRef(false);
const params = z
.object({ provider: zOAuthProvider() })
.safeParse(useParams());
const searchParams = z
.object({ code: z.string(), state: z.string() })
.safeParse({
code: useSearchParams().get('code'),
state: useSearchParams().get('state'),
});
const validateLogin = trpc.oauth.validateLogin.useMutation({
onSuccess: (data) => {
if (data.account.authorizations.includes('ADMIN')) {
router.replace(ROUTES_ADMIN.root());
return;
}
router.replace(ROUTES_APP.root());
},
onError: () => {
toastCustom({
status: 'error',
title: t('auth:login.feedbacks.loginError.title'),
});
router.replace(ROUTES_AUTH.login());
},
});

useEffect(() => {
const trigger = () => {
if (isTriggeredRef.current) return;
isTriggeredRef.current = true;

if (!(params.success && searchParams.success)) {
notFound();
}

validateLogin.mutate({
provider: params.data.provider,
code: searchParams.data.code,
state: searchParams.data.state,
language: i18n.language,
});
};
trigger();
}, [validateLogin, params, searchParams, i18n]);

return <LoaderFull />;
}
8 changes: 8 additions & 0 deletions src/features/auth/PageRegister.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
FormFieldLabel,
} from '@/components/Form';
import { toastCustom } from '@/components/Toast';
import {
OAuthLoginButtonsGrid,
OAuthLoginDivider,
} from '@/features/auth/OAuthLogin';
import { ROUTES_AUTH } from '@/features/auth/routes';
import {
FormFieldsRegister,
Expand Down Expand Up @@ -87,6 +91,10 @@ export default function PageRegister() {
</Box>
</Button>
</Stack>

<OAuthLoginButtonsGrid />
<OAuthLoginDivider />

<Form
{...form}
onSubmit={(values) => {
Expand Down
Loading

0 comments on commit 4d15427

Please sign in to comment.