Skip to content

Commit

Permalink
Merge pull request #20 from TickLabVN/featute/integrate-google-oauth
Browse files Browse the repository at this point in the history
Featute/integrate google oauth
  • Loading branch information
quannhg authored Nov 15, 2023
2 parents 9b04c47 + f42f5d0 commit 40f6f9d
Show file tree
Hide file tree
Showing 27 changed files with 456 additions and 87 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/dev-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ jobs:
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
mkdir -p $HOME/ssps-be && cd $HOME/ssps-be
rm -f .env
echo POSTGRES_USER=${{ secrets.POSTGRES_USER }} >> .env
echo POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} >> .env
Expand All @@ -85,12 +86,18 @@ jobs:
echo PAYPAL_SANDBOX_ENDPOINT=${{ vars.PAYPAL_SANDBOX_ENDPOINT }} >> .env
echo PAYPAL_CLIENT_ID=${{ secrets.PAYPAL_CLIENT_ID }} >> .env
echo PAYPAL_CLIENT_SECRET=${{ secrets.PAYPAL_CLIENT_SECRET }} >> .env
echo GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }} >> .env
echo GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }} >> .env
echo GOOGLE_REDIRECT_URL=${{ secrets.GOOGLE_REDIRECT_URL }} >> .env
echo UI_HOME_URL=${{ secrets.UI_HOME_URL }} >> .env
curl -s ${{secrets.DOCKER_COMPOSE_RAW_FILE_URL}} -O -f
docker compose stop ${{vars.DOCKER_COMPOSE_DEPLOY_SERVICE_NAME}}
docker compose down --volumes --remove-orphans
docker compose rm -f ${{vars.DOCKER_COMPOSE_DEPLOY_SERVICE_NAME}}
docker compose up -d ${{vars.DOCKER_COMPOSE_DEPLOY_SERVICE_NAME}}
docker exec -it $(docker ps --filter "ancestor=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -q) /bin/sh -c "npx prisma seed"
docker exec -it $(docker ps --filter "ancestor=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -q) /bin/sh -c "npx prisma db seed"
docker logout ${{ env.REGISTRY }}
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ jobs:
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
mkdir -p $HOME/ssps-be && cd $HOME/ssps-be
rm -f .env
echo POSTGRES_USER=${{ secrets.POSTGRES_USER }} >> .env
echo POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} >> .env
Expand All @@ -99,6 +100,11 @@ jobs:
echo PAYPAL_SANDBOX_ENDPOINT=${{ vars.PAYPAL_SANDBOX_ENDPOINT }} >> .env
echo PAYPAL_CLIENT_ID=${{ secrets.PAYPAL_CLIENT_ID }} >> .env
echo PAYPAL_CLIENT_SECRET=${{ secrets.PAYPAL_CLIENT_SECRET }} >> .env
echo GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }} >> .env
echo GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }} >> .env
echo GOOGLE_REDIRECT_URL=${{ secrets.GOOGLE_REDIRECT_URL }} >> .env
echo UI_HOME_URL=${{ secrets.UI_HOME_URL }} >> .env
curl -s ${{secrets.DOCKER_COMPOSE_RAW_FILE_URL}} -O -f
docker compose stop ${{vars.DOCKER_COMPOSE_DEPLOY_SERVICE_NAME}}
docker compose down --volumes --remove-orphans
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@
"axios": "^1.6.0",
"bcrypt": "^5.1.0",
"dotenv": "^16.3.1",
"envalid": "^7.3.1",
"envalid": "^8.0.0",
"fastify": "^4.21.0",
"fluent-json-schema": "^4.2.0-beta.0",
"googleapis": "^128.0.0",
"jsonwebtoken": "^9.0.1",
"minio": "^7.1.3",
"node-cache": "^5.1.2",
Expand All @@ -52,6 +53,7 @@
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@types/bcrypt": "^5.0.0",
"@types/find-config": "^1.0.4",
"@types/jest": "^29.5.3",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.4.8",
Expand Down
3 changes: 3 additions & 0 deletions prisma/migrations/20231111075656_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "userName" DROP NOT NULL,
ALTER COLUMN "password" DROP NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
6 changes: 3 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ datasource db {

model User {
id String @id @default(cuid())
userName String @unique @map("userName") @db.VarChar(50)
password String
userName String? @unique @map("userName") @db.VarChar(50)
password String?
role Int[]
name String
email String
email String @unique
Student Student[]
}

Expand Down
10 changes: 8 additions & 2 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,21 @@ const createPrintingRequest = async () => {
};

const createConfiguration = async () => {
const acceptedExtensions = ['.pdf', 'png'];
const configuration: {
name: string;
value: string;
description: string;
}[] = [
{ name: 'coin per page', value: '2', description: 'The amount of coin a student need to print one page' },
{ name: 'dollar to coin', value: '73', description: 'The amount of coin user get per dollar' }
{ name: 'coin per page', value: '2', description: 'The amount of coin a student needs to print one page' },
{ name: 'dollar to coin', value: '73', description: 'The amount of coin user gets per dollar' },
{ name: 'coin per sem', value: '100', description: 'The amount of coin a student has free in one semester' }
];

const serializedExtensions = JSON.stringify(acceptedExtensions);

configuration.push({ name: 'accepted extensions', value: serializedExtensions, description: 'Supported file extensions of printer' });

const sampleConfiguration = await prisma.configuration.createMany({ data: configuration });

console.log(sampleConfiguration);
Expand Down
3 changes: 2 additions & 1 deletion src/Server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import fastify, { FastifyInstance } from 'fastify';
import type { FastifyCookieOptions } from '@fastify/cookie';
import { customErrorHandler, envs, loggerConfig, swaggerConfig, swaggerUIConfig } from '@configs';
import { envs, loggerConfig, swaggerConfig, swaggerUIConfig } from '@configs';
import { apiPlugin, authPlugin } from './routes';
import { checkRoles } from '@hooks';
import { customErrorHandler } from '@handlers';

export function createServer(config: ServerConfig): FastifyInstance {
const app = fastify({ logger: loggerConfig[envs.NODE_ENV], ajv: { plugins: [require('@fastify/multipart').ajvFilePlugin] } });
Expand Down
22 changes: 0 additions & 22 deletions src/configs/coin.ts

This file was deleted.

26 changes: 18 additions & 8 deletions src/configs/env.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { config as configEnv } from 'dotenv';
import { str, num, cleanEnv } from 'envalid';
import { str, url, host, port, cleanEnv } from 'envalid';
import path from 'path';

configEnv();
const envFilePath = path.join(process.cwd(), '.env');
if (!envFilePath) {
console.error('.env file not found.');
process.exit(1);
}
configEnv({ path: envFilePath });

export const envs = cleanEnv(process.env, {
NODE_ENV: str<NodeEnv>({
Expand All @@ -11,17 +17,21 @@ export const envs = cleanEnv(process.env, {
JWT_SECRET: str(),
COOKIE_SECRET: str(),
CORS_WHITE_LIST: str(),
MINIO_URL: str(),
MINIO_SERVER_ENDPOINT: str(),
MINIO_PORT: num(),
MINIO_URL: url(),
MINIO_SERVER_ENDPOINT: host(),
MINIO_PORT: port(),
MINIO_ACCESS_KEY: str(),
MINIO_SECRET_KEY: str(),
MINIO_BUCKET_NAME: str(),
CHECKOUT_ENVIRONMENT: str(),
PAYPAL_LIVE_ENDPOINT: str(),
PAYPAL_SANDBOX_ENDPOINT: str(),
PAYPAL_LIVE_ENDPOINT: url(),
PAYPAL_SANDBOX_ENDPOINT: url(),
PAYPAL_CLIENT_ID: str(),
PAYPAL_CLIENT_SECRET: str()
PAYPAL_CLIENT_SECRET: str(),
GOOGLE_CLIENT_ID: str({ default: 'anc' }),
GOOGLE_CLIENT_SECRET: str(),
GOOGLE_REDIRECT_URL: url(),
UI_HOME_URL: url()
});

export const CORS_WHITE_LIST = envs.CORS_WHITE_LIST.split(',');
Expand Down
3 changes: 1 addition & 2 deletions src/configs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
* @file Automatically generated by barrelsby.
*/

export * from './coin';
export * from './env';
export * from './errorHandler';
export * from './logger';
export * from './printingRequest';
export * from './student';
export * from './swagger';
29 changes: 23 additions & 6 deletions src/configs/printingRequest.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import NodeCache from 'node-cache';
import { prisma } from '@repositories';
import { logger } from '@utils';

const cache = new NodeCache({ stdTTL: 300 });

//TODO: Please remove it if do not plan to use it in the future.
// export const PRINTING_CONFIGS = ['fileName', 'printingRequestId'];
// //TODO: Please remove it if do not plan to use it in the future.
// // export const PRINTING_CONFIGS = ['fileName', 'printingRequestId'];

export const COIN_PER_PAGE: Promise<number> = (async () => {
const cachedValue = cache.get('coinPerPage');
Expand All @@ -22,9 +21,27 @@ export const COIN_PER_PAGE: Promise<number> = (async () => {
cache.set('coinPerPage', coinPerPage);
return coinPerPage;
} catch (error) {
logger.error('Failed to retrieve "coin per page" configuration:', error);
return 200;
throw new Error('Failed to retrieve "coin per page" configuration:', error);
}
})();

export const ACCEPTED_EXTENSIONS = ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.jpg', '.png', '.pdf'];
export const ACCEPTED_EXTENSIONS: Promise<string[]> = (async () => {
try {
const acceptedExtensionsConfiguration = await prisma.configuration.findFirst({
select: { value: true },
where: { name: 'accepted extensions' }
});

if (!acceptedExtensionsConfiguration) {
throw new Error('No "accepted extensions" configuration found.');
}

const serializedExtensions = acceptedExtensionsConfiguration.value;

const acceptedExtensions = JSON.parse(serializedExtensions);

return acceptedExtensions;
} catch (error) {
throw new Error('Failed to retrieve "accepted extensions" configuration:', error);
}
})();
15 changes: 15 additions & 0 deletions src/configs/student.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { prisma } from '@repositories';

export const COIN_PER_SEM: Promise<number> = (async () => {
try {
const coinPerSemConfiguration = await prisma.configuration.findFirst({
select: { value: true },
where: { name: 'coin per sem' }
});
const coinPerSem = Number(coinPerSemConfiguration?.value) || 100;

return coinPerSem;
} catch (error) {
throw new Error('Failed to retrieve "coin per sem" configuration:', error);
}
})();
9 changes: 9 additions & 0 deletions src/dtos/in/auth/googleOauth.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Static, Type } from '@sinclair/typebox';

// See https://github.com/sinclairzx81/typebox

export const GoogleOAuthParamsDto = Type.Object({
code: Type.String()
});

export type GoogleOAuthParamsDto = Static<typeof GoogleOAuthParamsDto>;
1 change: 1 addition & 0 deletions src/dtos/in/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * from './checkout.dto';
export * from './printingRequest.dto';
export * from './uploadFile.dto ';
export * from './auth/auth.dto';
export * from './auth/googleOauth.dto';
export * from './auth/signUp.dto';
3 changes: 1 addition & 2 deletions src/dtos/out/auth.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { ObjectId } from '@dtos/common';
import { Static, Type } from '@sinclair/typebox';

export const AuthResultDto = Type.Object({
id: ObjectId,
userName: Type.String({ format: 'userName' })
id: ObjectId
});

export type AuthResultDto = Static<typeof AuthResultDto>;
69 changes: 60 additions & 9 deletions src/handlers/auth.handler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { compare, hash } from 'bcrypt';
import { prisma } from '@repositories';
import { cookieOptions, DUPLICATED_userName, LOGIN_FAIL, SALT_ROUNDS, USER_NOT_FOUND } from '@constants';
import { cookieOptions, DUPLICATED_userName, LOGIN_FAIL, SALT_ROUNDS, USER_NOT_FOUND, USER_ROLES } from '@constants';
import jwt from 'jsonwebtoken';
import { envs } from '@configs';
import { COIN_PER_SEM, envs } from '@configs';
import { User } from '@prisma/client';
import { AuthInputDto, SignUpRequestDto } from '@dtos/in';
import { AuthInputDto, GoogleOAuthParamsDto, SignUpRequestDto } from '@dtos/in';
import { AuthResultDto } from '@dtos/out';
import { Handler } from '@interfaces';
import { logger } from '@utils';
import { getUserInfo, logger } from '@utils';
import { UserRole } from 'src/types/auth';

const login: Handler<AuthResultDto, { Body: AuthInputDto }> = async (req, res) => {
const user = await prisma.user.findUnique({
Expand All @@ -21,15 +22,15 @@ const login: Handler<AuthResultDto, { Body: AuthInputDto }> = async (req, res) =
});
if (!user) return res.badRequest(USER_NOT_FOUND);

if (!user.password) return res.badRequest('User is not valid!');
const correctPassword = await compare(req.body.password, user.password);
if (!correctPassword) return res.badRequest(LOGIN_FAIL);

const userToken = jwt.sign({ userId: user.id, roles: user.role }, envs.JWT_SECRET);
res.setCookie('token', userToken, cookieOptions);

return {
id: user.id,
userName: user.userName
id: user.id
};
};

Expand All @@ -55,12 +56,62 @@ const signup: Handler<AuthResultDto, { Body: SignUpRequestDto }> = async (req, r
res.setCookie('token', userToken, cookieOptions);

return {
id: user.id,
userName: user.userName
id: user.id
};
};

const createStudent = async (userData: { name: string; email: string; role: UserRole[] }) => {
return prisma.$transaction(async (prisma) => {
const user = await prisma.user.create({
data: userData,
select: { id: true }
});

await prisma.student.create({
data: { default_coin_per_sem: await COIN_PER_SEM, remain_coin: await COIN_PER_SEM, id: user.id }
});

return user;
});
};

const googleOAuth: Handler<AuthResultDto, { Querystring: GoogleOAuthParamsDto }> = async (req, res) => {
try {
const { userEmail, userName, isVerifiedEmail } = await getUserInfo(req.query.code);

if (!isVerifiedEmail) {
return res.status(406).send('Email needs to be verified for authentication.');
}

if (userEmail && userName) {
const user = await prisma.user.findUnique({
where: { email: userEmail },
select: { id: true }
});

const userData = {
name: userName,
email: userEmail,
role: [USER_ROLES.student]
};

const userId = user ? user.id : (await createStudent(userData)).id;

const userToken = jwt.sign({ userId: userId }, envs.JWT_SECRET);
res.setCookie('token', userToken, cookieOptions);

return res.redirect(envs.UI_HOME_URL).send({ id: userId });
} else {
res.status(400).send('User information not available.');
}
} catch (error) {
console.error('Error processing user information:', error);
res.status(500).send('Error processing user information');
}
};

export const authHandler = {
login,
signup
signup,
googleOAuth
};
Loading

0 comments on commit 40f6f9d

Please sign in to comment.