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

feat: Dodano możliwość autoryzacji użytkowników #87

Merged
merged 14 commits into from
Jan 2, 2021
7 changes: 6 additions & 1 deletion .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ DATABASE_URL="postgresql://postgres:polskifrontend@localhost:5432/polskifrontend
FEED_UPDATE_SECRET="supertajne"
CURSOR_ENCRYPTION_KEY="d6F3Efeqd6F3Efeq"


CAPTCHA_SECRET_KEY=0x0000000000000000000000000000000000000000

MAILGUN_DOMAIN="domena1337.mailgun.org"
Expand All @@ -14,3 +13,9 @@ NEXT_PUBLIC_GA_TRACKING_ID=""
NEXT_PUBLIC_SENTRY_DSN=""
NEXT_PUBLIC_CAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001
NEXT_PUBLIC_URL="localhost:3000"

GITHUB_ID="supertajne"
GITHUB_SECRET="bardzotajne"
SECRET="anothersecret"
JWT_SECRET="hmmmmmm"
NEXTAUTH_URL="localhost:3000"
wisnie marked this conversation as resolved.
Show resolved Hide resolved
61 changes: 61 additions & 0 deletions api-helpers/prisma/migrations/20201230195946_auth/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
Warnings:

- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.

*/
-- DropForeignKey
ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey";

-- CreateTable
CREATE TABLE "accounts" (
"id" SERIAL,
"compound_id" TEXT NOT NULL,
"user_id" INTEGER NOT NULL,
"provider_type" TEXT NOT NULL,
"provider_id" TEXT NOT NULL,
"provider_account_id" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"access_token_expires" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "users" (
"id" SERIAL,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"image" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"role" "UserRole" NOT NULL DEFAULT E'USER',

PRIMARY KEY ("id")
);

-- DropTable
DROP TABLE "Session";

-- DropTable
DROP TABLE "User";

-- CreateIndex
CREATE UNIQUE INDEX "accounts.compound_id_unique" ON "accounts"("compound_id");

-- CreateIndex
CREATE INDEX "providerAccountId" ON "accounts"("provider_account_id");

-- CreateIndex
CREATE INDEX "providerId" ON "accounts"("provider_id");

-- CreateIndex
CREATE INDEX "userId" ON "accounts"("user_id");

-- CreateIndex
CREATE UNIQUE INDEX "users.email_unique" ON "users"("email");
47 changes: 30 additions & 17 deletions api-helpers/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,40 @@ datasource db {
url = env("DATABASE_URL")
}

model User {
id String @default(cuid()) @id
name String?
email String @unique
password String

role UserRole @default(USER)

createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
model Account {
id Int @default(autoincrement()) @id
compoundId String @unique @map(name: "compound_id")
userId Int @map(name: "user_id")
providerType String @map(name: "provider_type")
providerId String @map(name: "provider_id")
providerAccountId String @map(name: "provider_account_id")
refreshToken String? @map(name: "refresh_token")
accessToken String? @map(name: "access_token")
accessTokenExpires DateTime? @map(name: "access_token_expires")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")

@@index([providerAccountId], name: "providerAccountId")
@@index([providerId], name: "providerId")
@@index([userId], name: "userId")

@@map(name: "accounts")
}

model Session {
id String @id
validUntil DateTime

userId String
user User @relation(fields: [userId], references: [id])

createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
model User {
id Int @default(autoincrement()) @id
name String?
email String? @unique
emailVerified DateTime? @map(name: "email_verified")
image String?
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")

role UserRole @default(USER)

@@map(name: "users")
}

enum UserRole {
Expand Down
19 changes: 19 additions & 0 deletions components/AdminPanel/AdminPanel.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.section {
max-width: 73rem;
margin: 0 auto;
}

.heading {
padding: 2rem 0 1rem;
border-bottom: 2px solid var(--gray-border);
text-align: center;
text-transform: uppercase;
color: var(--gray-text);
font-size: 1.25rem;
}

.p {
text-align: center;
color: var(--gray-text);
font-size: 1.25rem;
}
34 changes: 34 additions & 0 deletions components/AdminPanel/AdminPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { signIn, useSession } from 'next-auth/client';
import { useEffect } from 'react';

import { LoadingScreen } from '../LoadingScreen/LoadingScreen';

import styles from './AdminPanel.module.css';

export const AdminPanel = () => {
const [session, isLoading] = useSession();

useEffect(() => {
if (!isLoading && !session) {
void signIn();
}
}, [session, isLoading]);

if (isLoading) {
return <LoadingScreen />;
}

if (session?.user.role === 'ADMIN') {
return <section></section>; // @to-do admin-panel
}

return (
<section className={styles.section}>
<h2 className={styles.heading}>Brak uprawnień</h2>
<p className={styles.p}>
Nie masz odpowiednich uprawnień, żeby korzystać z tej podstrony, w celu weryfikacji
wisnie marked this conversation as resolved.
Show resolved Hide resolved
skontaktuj się z administracją serwisu.
</p>
</section>
);
};
8 changes: 8 additions & 0 deletions components/LoadingScreen/LoadingScreen.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.loadingScreen {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
inset: 0;
typeofweb marked this conversation as resolved.
Show resolved Hide resolved
background-color: var(--white);
}
9 changes: 9 additions & 0 deletions components/LoadingScreen/LoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styles from './LoadingScreen.module.css';

export const LoadingScreen = () => {
return (
<section className={styles.loadingScreen}>
<img src="/logo.svg" alt="Ładowanie..." width={320} height={100} />
</section>
);
};
9 changes: 9 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ declare namespace NodeJS {
readonly NEXT_PUBLIC_URL: string;
}
}

type Role = 'USER' | 'ADMIN';

declare module 'next-auth' {
interface User {
readonly userId?: number;
readonly role?: Role;
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"mailgun-js": "0.22.0",
"ms": "2.1.3",
"next": "10.0.4",
"next-auth": "3.1.0",
"next-images": "1.6.2",
"next-seo": "4.17.0",
"normalize.css": "8.0.1",
Expand Down Expand Up @@ -81,6 +82,7 @@
"@types/mailgun-js": "0.22.11",
"@types/mongodb": "3.6.3",
"@types/ms": "0.7.31",
"@types/next-auth": "3.1.19",
"@types/node": "14.14.16",
"@types/pino": "6.3.4",
"@types/react": "17.0.0",
Expand Down
5 changes: 4 additions & 1 deletion pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'normalize.css/normalize.css';
import '../global.scss';
import '../icomoon-v1.0/style.css';
import { Provider } from 'next-auth/client';
import { DefaultSeo } from 'next-seo';
import type { AppProps } from 'next/app';
import Head from 'next/head';
Expand Down Expand Up @@ -77,7 +78,9 @@ export default function MyApp({
<link rel="manifest" href="/manifest.json" />
<link rel="alternate" href="/feed" type="application/rss+xml" title="Polski Frontend RSS" />
</Head>
<Component {...pageProps} err={err} />
<Provider session={pageProps.session}>
wisnie marked this conversation as resolved.
Show resolved Hide resolved
<Component {...pageProps} err={err} />
</Provider>
</>
);
}
10 changes: 10 additions & 0 deletions pages/admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AdminPanel } from '../components/AdminPanel/AdminPanel';
typeofweb marked this conversation as resolved.
Show resolved Hide resolved
import { Layout } from '../components/Layout';

export default function AdminPage() {
return (
<Layout title="Panel admina">
<AdminPanel />
</Layout>
);
}
69 changes: 69 additions & 0 deletions pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Boom from '@hapi/boom';
import type { NextApiHandler } from 'next';
import type { InitOptions } from 'next-auth';
import NextAuth from 'next-auth';
import Adapters from 'next-auth/adapters';
import Providers from 'next-auth/providers';

import { closeConnection, openConnection } from '../../../api-helpers/db';

const authHandler: NextApiHandler = async (req, res) => {
const prisma = await openConnection();
typeofweb marked this conversation as resolved.
Show resolved Hide resolved
const options: InitOptions = {
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID as string,
clientSecret: process.env.GITHUB_SECRET as string,
}),
],
secret: process.env.SECRET,
adapter: Adapters.Prisma.Adapter({
prisma,
}),
session: {
jwt: true,
maxAge: 24 * 60 * 60, // 1 day
},
jwt: {
secret: process.env.JWT_SECRET,
},
callbacks: {
jwt: async (token, user?) => {
if (!user) {
// If it's not sign in operation return already created token
return Promise.resolve(token);
}

const userId = user.userId;
try {
const user = await prisma.user.findUnique({
typeofweb marked this conversation as resolved.
Show resolved Hide resolved
where: {
id: userId,
},
wisnie marked this conversation as resolved.
Show resolved Hide resolved
});
token.userId = userId;
token.role = user?.role;
return Promise.resolve(token);
} catch (err) {
throw Boom.unauthorized();
} finally {
await closeConnection();
}
},
session: (session, token) => {
return Promise.resolve({
...session,
user: {
...session.user,
role: token.role,
userId: token.userId,
},
});
},
},
};

return NextAuth(req, res, options);
};

export default authHandler;
Loading