diff --git a/public/assets/mirea.svg b/public/assets/mirea.svg new file mode 100644 index 0000000..782b709 --- /dev/null +++ b/public/assets/mirea.svg @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/form/sidebar-search-input.tsx b/src/components/form/sidebar-search-input.tsx index 9709f4e..5fa9c95 100644 --- a/src/components/form/sidebar-search-input.tsx +++ b/src/components/form/sidebar-search-input.tsx @@ -21,7 +21,7 @@ export default function SidebarSearchInput({ id='search' className='block w-full cursor-pointer rounded-2xl border-gray-400 bg-transparent py-2 pl-8 pr-12 placeholder-gray-500 caret-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm' placeholder='Быстрый поиск' - disabled + readOnly />
diff --git a/src/components/layout/command-palette.tsx b/src/components/layout/command-palette.tsx index 477f899..b4537df 100644 --- a/src/components/layout/command-palette.tsx +++ b/src/components/layout/command-palette.tsx @@ -52,10 +52,10 @@ export default function CommandPalette({ open, setOpen }: CommandPaletteProps) { }, ) - const fetchPost = (postId: number, reason: PostItemReason) => { + const fetchPost = (slug: string, reason: PostItemReason) => { return () => { setOpen(false) - void router.push(reason === PostItemReason.FOUND ? `/finds/${postId}` : `/losses/${postId}`) + void router.push(reason === PostItemReason.FOUND ? `/finds/${slug}` : `/losses/${slug}`) } } @@ -65,13 +65,13 @@ export default function CommandPalette({ open, setOpen }: CommandPaletteProps) { const onSelectedOptionEnter = ( open: boolean, - selectedOption: { id: number; reason: PostItemReason } | null, + selectedOption: { id: number; slug: string; reason: PostItemReason } | null, ) => { return (event: KeyboardEventHandler) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (open && selectedOption && event.key === 'Enter') { - fetchPost(selectedOption.id, selectedOption.reason)() + fetchPost(selectedOption.slug, selectedOption.reason)() } } } @@ -117,7 +117,11 @@ export default function CommandPalette({ open, setOpen }: CommandPaletteProps) { onKeyDown={(e: any) => onSelectedOptionEnter( open, - activeOption as { id: number; reason: PostItemReason } | null, + activeOption as { + id: number + slug: string + reason: PostItemReason + } | null, // eslint-disable-next-line @typescript-eslint/no-unsafe-argument )(e) } @@ -156,7 +160,7 @@ export default function CommandPalette({ open, setOpen }: CommandPaletteProps) { classNames( 'cursor-default select-none px-4 py-2', diff --git a/src/components/layout/layout.tsx b/src/components/layout/layout.tsx index 7ff97e7..3b26e4e 100644 --- a/src/components/layout/layout.tsx +++ b/src/components/layout/layout.tsx @@ -102,8 +102,8 @@ export default function Layout(props: LayoutProps) { Mirea Ninja diff --git a/src/components/logo.tsx b/src/components/logo.tsx index bcf7ede..b5298e5 100644 --- a/src/components/logo.tsx +++ b/src/components/logo.tsx @@ -7,8 +7,8 @@ export default function Logo() { Mirea Ninja diff --git a/src/components/profile/edit-profile-slide-over/edit-profile-slide-over-body.tsx b/src/components/profile/edit-profile-slide-over/edit-profile-slide-over-body.tsx index 4517715..2faf9c0 100644 --- a/src/components/profile/edit-profile-slide-over/edit-profile-slide-over-body.tsx +++ b/src/components/profile/edit-profile-slide-over/edit-profile-slide-over-body.tsx @@ -214,7 +214,13 @@ const SocialNetworkDialogButton = ({
- {network} + {network} {socialNetworkName}
{isLinked ? ( diff --git a/src/components/profile/edit-profile-slide-over/edit-profile-slide-over.tsx b/src/components/profile/edit-profile-slide-over/edit-profile-slide-over.tsx index 80753d6..398874e 100644 --- a/src/components/profile/edit-profile-slide-over/edit-profile-slide-over.tsx +++ b/src/components/profile/edit-profile-slide-over/edit-profile-slide-over.tsx @@ -96,8 +96,7 @@ export default function EditProfileSlideOver(props: EditProfileSlideOverProps) { ) } > -
-
+
diff --git a/src/components/profile/profile-body/profile-body.tsx b/src/components/profile/profile-body/profile-body.tsx index fe77489..27c2e74 100644 --- a/src/components/profile/profile-body/profile-body.tsx +++ b/src/components/profile/profile-body/profile-body.tsx @@ -24,7 +24,7 @@ export default function ProfileBody() { if (router.query.open === 'socials') { editProfile.open() } - }, [editProfile, router.query.open]) + }, [router.query.open]) const profileInfo: { name: string; value: ReactNode }[] = user ? [ diff --git a/src/components/seo/default-seo.tsx b/src/components/seo/default-seo.tsx index d74fe82..d0bf8ed 100644 --- a/src/components/seo/default-seo.tsx +++ b/src/components/seo/default-seo.tsx @@ -7,12 +7,12 @@ type DefaultSeoProps = Partial, 'title export default function DefaultSeo(props: DefaultSeoProps) { return ( { + switch (provider) { + case 'mirea': + return '/assets/providers/mirea.svg' + case 'google': + return '/assets/providers/google.svg' + case 'github': + return '/assets/providers/github.svg' + default: + return '/assets/providers/mirea.svg' + } +} export default function SignIn() { const router = useRouter() const callbackUrl = router.query.callbackUrl ? (router.query.callbackUrl as string) : '/' const error = router.query.error - const providers: { id: string; name: string; image: string }[] = [ - { - id: 'mirea', - name: 'ЛКC', - image: '/assets/providers/mirea.svg', - }, - { - id: 'google', - name: 'Google', - image: '/assets/providers/google.svg', + const [providers, setProviders] = React.useState<{ id: string; name: string; image: string }[]>( + [], + ) + + useQuery( + ['providers'], + async () => { + const providers = await getProviders() + return providers }, { - id: 'github', - name: 'GitHub', - image: '/assets/providers/github.svg', + onSuccess: (providers) => { + if (!providers) return + + setProviders( + Object.values(providers).map((provider) => ({ + id: provider.id, + name: provider.name, + image: getProviderImage(provider.id), + })), + ) + }, }, - ] + ) return (
diff --git a/src/env.mjs b/src/env.mjs index 48008c2..a823e42 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -25,12 +25,18 @@ export const env = createEnv({ REDIS_URL: z.string().url(), NEXTAUTH_SECRET: z.string().min(1), NEXTAUTH_URL: z.string().url(), - MIREA_CLIENT_ID: z.string().min(1), - MIREA_CLIENT_SECRET: z.string().min(1), - GOOGLE_CLIENT_ID: z.string().min(1), - GOOGLE_CLIENT_SECRET: z.string().min(1), - GITHUB_CLIENT_ID: z.string().min(1), - GITHUB_CLIENT_SECRET: z.string().min(1), + + MIREA_CLIENT_ID: z.string().nullish(), + MIREA_CLIENT_SECRET: z.string().nullish(), + + MIREA_LKS_CLIENT_ID: z.string().nullish(), + MIREA_LKS_CLIENT_SECRET: z.string().nullish(), + + GOOGLE_CLIENT_ID: z.string().nullish(), + GOOGLE_CLIENT_SECRET: z.string().nullish(), + + GITHUB_CLIENT_ID: z.string().nullish(), + GITHUB_CLIENT_SECRET: z.string().nullish(), CALLBACK_URL: z.string().url(), CALLBACK_SECRET_URL_STRING: z.string().min(1).max(32), @@ -86,6 +92,10 @@ export const env = createEnv({ MIREA_CLIENT_ID: process.env.MIREA_CLIENT_ID, MIREA_CLIENT_SECRET: process.env.MIREA_CLIENT_SECRET, + + MIREA_LKS_CLIENT_ID: process.env.MIREA_LKS_CLIENT_ID, + MIREA_LKS_CLIENT_SECRET: process.env.MIREA_LKS_CLIENT_SECRET, + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, diff --git a/src/server/api/routers/posts.ts b/src/server/api/routers/posts.ts index acd6ce1..bffab4f 100644 --- a/src/server/api/routers/posts.ts +++ b/src/server/api/routers/posts.ts @@ -354,6 +354,7 @@ async function searchPosts(query: string, reason: PostItemReason) { name: true, description: true, reason: true, + slug: true, }, where: { status, @@ -376,6 +377,7 @@ async function searchPosts(query: string, reason: PostItemReason) { name: true, description: true, reason: true, + slug: true, }, where: { status, diff --git a/src/server/auth-providers/mirea-ninja-lks-provider.ts b/src/server/auth-providers/mirea-ninja-lks-provider.ts index 63c7d30..9b14f8c 100644 --- a/src/server/auth-providers/mirea-ninja-lks-provider.ts +++ b/src/server/auth-providers/mirea-ninja-lks-provider.ts @@ -21,8 +21,8 @@ type MireaNinjaLKSProviderConfig = Required, 'clientId' | export default function MireaNinjaLksProvider(options: MireaNinjaLKSProviderConfig): Provider { return { - id: 'mirea', - name: 'Mirea', + id: 'lks', + name: 'LKS', type: 'oauth', version: '2.0', accessTokenUrl: 'https://auth-app.mirea.ru/oauth/token', @@ -51,7 +51,6 @@ export default function MireaNinjaLksProvider(options: MireaNinjaLKSProviderConf image: 'https://lk.mirea.ru' + profile.arUser.PHOTO, isBlocked: false, blockReason: null, - secretSocialNetworksAuthPayload: '', } }, clientId: options.clientId, diff --git a/src/server/auth-providers/mirea-provider.ts b/src/server/auth-providers/mirea-provider.ts new file mode 100644 index 0000000..31110f6 --- /dev/null +++ b/src/server/auth-providers/mirea-provider.ts @@ -0,0 +1,53 @@ +import type { OAuthConfig, Provider } from 'next-auth/providers' +import { nicknameValidation } from '@/lib/nickname-validation' +import { Role } from '@prisma/client' + +interface UserInfo { + username: string + email: string + name: string + lastname: string + middlename: string + uid: string +} + +type MireaProviderConfig = Required, 'clientId' | 'clientSecret'>> + +export default function MireaProvider(options: MireaProviderConfig): Provider { + return { + id: 'mirea', + name: 'RTU MIREA', + type: 'oauth', + version: '2.0', + accessTokenUrl: 'https://login.mirea.ru/oauth2/v1/token/', + requestTokenUrl: 'https://login.mirea.ru/oauth2/v1/token/', + authorization: { + url: 'https://login.mirea.ru/oauth2/v1/authorize/', + params: { scope: 'basic' }, + }, + token: { + url: 'https://login.mirea.ru/oauth2/v1/token/', + }, + userinfo: { + url: 'https://login.mirea.ru/resources/v1/userinfo', + }, + checks: ['state'], + async profile(profile: UserInfo) { + const name = [profile.name, profile.lastname].join(' ') + return { + id: profile.uid, + name, + nickname: await nicknameValidation(name), + email: profile.email, + emailVerified: new Date(), + userInfo: null, + role: Role.USER, + image: null, + isBlocked: false, + blockReason: null, + } + }, + clientId: options.clientId, + clientSecret: options.clientSecret, + } +} diff --git a/src/server/auth.ts b/src/server/auth.ts index ae095bb..d6b808d 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -9,6 +9,7 @@ import { Role } from '@prisma/client' import { env } from '@/env.mjs' import { type User as PrismaUser } from '@prisma/client' import MireaNinjaLksProvider from '@/server/auth-providers/mirea-ninja-lks-provider' +import MireaProvider from './auth-providers/mirea-provider' /** * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` @@ -17,12 +18,82 @@ import MireaNinjaLksProvider from '@/server/auth-providers/mirea-ninja-lks-provi * @see https://next-auth.js.org/getting-started/typescript#module-augmentation */ declare module 'next-auth' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface User extends Omit {} + interface Session { user: User } +} - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface User extends PrismaUser {} +const getProviders = () => { + const providers = [] + if (env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET) { + providers.push( + GithubProvider({ + clientId: env.GITHUB_CLIENT_ID, + clientSecret: env.GITHUB_CLIENT_SECRET, + async profile(profile: GithubProfile) { + return { + id: profile.id.toString(), + name: profile.name ?? profile.login, + nickname: await nicknameValidation(profile.login), + email: profile.email, + emailVerified: new Date(), + userInfo: null, + role: Role.USER, + image: profile.avatar_url, + isBlocked: false, + blockReason: null, + } + }, + }), + ) + } + if (env.MIREA_CLIENT_ID && env.MIREA_CLIENT_SECRET) { + providers.push( + MireaProvider({ clientId: env.MIREA_CLIENT_ID, clientSecret: env.MIREA_CLIENT_SECRET }), + ) + } + if (env.MIREA_LKS_CLIENT_ID && env.MIREA_LKS_CLIENT_SECRET) { + providers.push( + MireaNinjaLksProvider({ + clientId: env.MIREA_LKS_CLIENT_ID, + clientSecret: env.MIREA_LKS_CLIENT_SECRET, + }), + ) + } + if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) { + providers.push( + GoogleProvider({ + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, + authorization: { + params: { + prompt: 'consent', + access_type: 'offline', + response_type: 'code', + }, + }, + async profile(profile: GoogleProfile) { + return { + id: profile.sub, + name: profile.name, + nickname: await nicknameValidation(profile.email.split('@')[0] ?? ''), + email: profile.email, + emailVerified: new Date(), + userInfo: null, + role: Role.USER, + image: profile.picture, + isBlocked: false, + blockReason: null, + } + }, + }), + ) + } + + return providers } /** @@ -52,52 +123,7 @@ export const authOptions: NextAuthOptions = { }, adapter: PrismaAdapter(prisma), providers: [ - GithubProvider({ - clientId: env.GITHUB_CLIENT_ID, - clientSecret: env.GITHUB_CLIENT_SECRET, - async profile(profile: GithubProfile) { - return { - id: profile.id.toString(), - name: profile.name ?? profile.login, - nickname: await nicknameValidation(profile.login), - email: profile.email, - emailVerified: new Date(), - userInfo: null, - role: Role.USER, - image: profile.avatar_url, - isBlocked: false, - blockReason: null, - secretSocialNetworksAuthPayload: '', - } - }, - }), - GoogleProvider({ - clientId: env.GOOGLE_CLIENT_ID, - clientSecret: env.GOOGLE_CLIENT_SECRET, - authorization: { - params: { - prompt: 'consent', - access_type: 'offline', - response_type: 'code', - }, - }, - async profile(profile: GoogleProfile) { - return { - id: profile.sub, - name: profile.name, - nickname: await nicknameValidation(profile.email.split('@')[0] ?? ''), - email: profile.email, - emailVerified: new Date(), - userInfo: null, - role: Role.USER, - image: profile.picture, - isBlocked: false, - blockReason: null, - secretSocialNetworksAuthPayload: '', - } - }, - }), - MireaNinjaLksProvider({ clientId: env.MIREA_CLIENT_ID, clientSecret: env.MIREA_CLIENT_SECRET }), + ...getProviders(), /** * ...add more providers here. *