Merge pull request #234 from Chroma-Case/feat/google

This commit is contained in:
Zoe Roux
2023-07-24 19:40:15 +09:00
committed by GitHub
18 changed files with 289 additions and 18 deletions

View File

@@ -7,4 +7,6 @@ JWT_SECRET=wow
POSTGRES_DB=chromacase
API_URL=http://localhost:80/api
SCORO_URL=ws://localhost:6543
GOOGLE_CLIENT_ID=toto
GOOGLE_SECRET=tata
GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google

View File

@@ -42,7 +42,7 @@ jobs:
- name: Install dependencies
run: yarn install
- name: Type Check
run: yarn tsc
- name: Check Prettier
@@ -84,16 +84,7 @@ jobs:
fetch-depth: 0
- name: Copy env file to github secret env file
run: |
touch .env
echo "POSTGRES_USER=user" >> .env
echo "POSTGRES_PASSWORD=eip" >> .env
echo "POSTGRES_NAME=chromacase" >> .env
echo "POSTGRES_HOST=db" >> .env
echo "DATABASE_URL=postgresql://user:eip@db:5432/chromacase" >> .env
echo "JWT_SECRET=wow" >> .env
echo "POSTGRES_DB=chromacase" >> .env
echo "API_URL=http://localhost:80/api" >> .env
run: cp .env.example .env
- name: Start the service
run: docker-compose up -d back db
@@ -101,6 +92,7 @@ jobs:
- name: Perform healthchecks
run: |
docker-compose ps -a
docker-compose logs
wget --retry-connrefused http://localhost:3000 # /healthcheck
- name: Run scorometer tests

147
back/package-lock.json generated
View File

@@ -24,6 +24,7 @@
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
@@ -38,6 +39,7 @@
"@types/express": "^4.17.13",
"@types/jest": "27.4.1",
"@types/node": "^16.0.0",
"@types/passport-google-oauth20": "^2.0.11",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
@@ -1962,6 +1964,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.33.tgz",
"integrity": "sha512-0PJ0vg+JyU0MIan58IOIFRtSvsb7Ri+7Wltx2qAg94eMOrpg4+uuP3aUHCpxXc1i0jCXiC+zIamSZh3l9AbcQA=="
},
"node_modules/@types/oauth": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz",
"integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -1976,6 +1987,28 @@
"@types/express": "*"
}
},
"node_modules/@types/passport-google-oauth20": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.11.tgz",
"integrity": "sha512-9XMT1GfwhZL7UQEiCepLef55RNPHkbrCtsU7rsWPTEOsmu5qVIW8nSemtB4p+P24CuOhA+IKkv8LsPThYghGww==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-oauth2": "*"
}
},
"node_modules/@types/passport-oauth2": {
"version": "1.4.12",
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.12.tgz",
"integrity": "sha512-RZg6cYTyEGinrZn/7REYQds6zrTxoBorX1/fdaz5UHzkG8xdFE7QQxkJagCr2ETzGII58FAFDmnmbTUVMrltNA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/oauth": "*",
"@types/passport": "*"
}
},
"node_modules/@types/prettier": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.0.tgz",
@@ -2763,6 +2796,14 @@
}
]
},
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@@ -6870,6 +6911,11 @@
"integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
"dev": true
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -7113,6 +7159,17 @@
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
"integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-jwt": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz",
@@ -7133,6 +7190,25 @@
"node": ">= 0.4.0"
}
},
"node_modules/passport-oauth2": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
"integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==",
"dependencies": {
"base64url": "3.x.x",
"oauth": "0.9.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
@@ -8775,6 +8851,11 @@
"node": ">=4.2.0"
}
},
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA=="
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -10691,6 +10772,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.33.tgz",
"integrity": "sha512-0PJ0vg+JyU0MIan58IOIFRtSvsb7Ri+7Wltx2qAg94eMOrpg4+uuP3aUHCpxXc1i0jCXiC+zIamSZh3l9AbcQA=="
},
"@types/oauth": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz",
"integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -10705,6 +10795,28 @@
"@types/express": "*"
}
},
"@types/passport-google-oauth20": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.11.tgz",
"integrity": "sha512-9XMT1GfwhZL7UQEiCepLef55RNPHkbrCtsU7rsWPTEOsmu5qVIW8nSemtB4p+P24CuOhA+IKkv8LsPThYghGww==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-oauth2": "*"
}
},
"@types/passport-oauth2": {
"version": "1.4.12",
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.12.tgz",
"integrity": "sha512-RZg6cYTyEGinrZn/7REYQds6zrTxoBorX1/fdaz5UHzkG8xdFE7QQxkJagCr2ETzGII58FAFDmnmbTUVMrltNA==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/oauth": "*",
"@types/passport": "*"
}
},
"@types/prettier": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.0.tgz",
@@ -11301,6 +11413,11 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@@ -14452,6 +14569,11 @@
"integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
"dev": true
},
"oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -14624,6 +14746,14 @@
"utils-merge": "^1.0.1"
}
},
"passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
"integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
"requires": {
"passport-oauth2": "1.x.x"
}
},
"passport-jwt": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz",
@@ -14641,6 +14771,18 @@
"passport-strategy": "1.x.x"
}
},
"passport-oauth2": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
"integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==",
"requires": {
"base64url": "3.x.x",
"oauth": "0.9.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
@@ -15831,6 +15973,11 @@
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
"dev": true
},
"uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA=="
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",

View File

@@ -36,6 +36,7 @@
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
@@ -50,6 +51,7 @@
"@types/express": "^4.17.13",
"@types/jest": "27.4.1",
"@types/node": "^16.0.0",
"@types/passport-google-oauth20": "^2.0.11",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[googleID]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "googleID" TEXT,
ALTER COLUMN "password" DROP NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "User_googleID_key" ON "User"("googleID");

View File

@@ -12,8 +12,9 @@ datasource db {
model User {
id Int @id @default(autoincrement())
username String @unique
password String
password String?
email String
googleID String? @unique
isGuest Boolean @default(false)
partyPlayed Int @default(0)
LessonHistory LessonHistory[]

View File

@@ -12,6 +12,7 @@ import {
InternalServerErrorException,
Patch,
NotFoundException,
Req,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@@ -32,6 +33,7 @@ import { Profile } from './dto/profile.dto';
import { Setting } from 'src/models/setting';
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
import { SettingsService } from 'src/settings/settings.service';
import { AuthGuard } from '@nestjs/passport';
@ApiTags('auth')
@Controller('auth')
@@ -42,6 +44,21 @@ export class AuthController {
private settingsService: SettingsService,
) {}
@Get("login/google")
@UseGuards(AuthGuard('google'))
googleLogin() { }
@Get("logged/google")
@UseGuards(AuthGuard('google'))
async googleLoginCallbakc(@Req() req: any) {
let user = await this.usersService.user({googleID: req.user.googleID});
if (!user) {
user = await this.usersService.createUser(req.user)
await this.settingsService.createUserSetting(user.id);
}
return this.authService.login(user);
}
@Post('register')
async register(@Body() registerDto: RegisterDto): Promise<void> {
try {

View File

@@ -9,6 +9,7 @@ import { ConfigModule } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { JwtStrategy } from './jwt.strategy';
import { SettingsModule } from 'src/settings/settings.module';
import { GoogleStrategy } from './google.strategy';
@Module({
imports: [
@@ -25,7 +26,7 @@ import { SettingsModule } from 'src/settings/settings.module';
inject: [ConfigService],
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
providers: [AuthService, LocalStrategy, JwtStrategy, GoogleStrategy],
controllers: [AuthController],
})
export class AuthModule {}

View File

@@ -15,7 +15,7 @@ export class AuthService {
password: string,
): Promise<PayloadInterface | null> {
const user = await this.userService.user({ username });
if (user && bcrypt.compareSync(password, user.password)) {
if (user && user.password && bcrypt.compareSync(password, user.password)) {
return {
username: user.username,
id: user.id,

View File

@@ -0,0 +1,35 @@
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ['email', 'profile'],
});
}
async validate(
_accessToken: string,
_refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const user = {
email: profile.emails[0].value,
username: profile.displayName,
password: null,
googleID: profile.id,
// firstName: name.givenName,
// lastName: name.familyName,
// picture: photos[0].value,
};
done(null, user);
return user;
}
}

View File

@@ -34,7 +34,8 @@ export class UsersService {
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
data.password = await bcrypt.hash(data.password, 8);
if (data.password)
data.password = await bcrypt.hash(data.password, 8);
return this.prisma.user.create({
data,
});

View File

@@ -28,6 +28,7 @@ import { Button, Center, VStack } from 'native-base';
import { unsetAccessToken } from './state/UserSlice';
import TextButton from './components/TextButton';
import ErrorView from './views/ErrorView';
import GoogleView from './views/GoogleView';
// Util function to hide route props in URL
const removeMe = () => '';
@@ -100,6 +101,11 @@ const publicRoutes = () =>
options: { title: 'Oops', headerShown: false },
link: undefined,
},
Google: {
component: GoogleView,
options: { title: 'Google signin', headerShown: false },
link: '/logged/google',
},
} as const);
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -7,6 +7,7 @@ export const en = {
signOutBtn: 'Sign out',
signInBtn: 'Sign in',
signUpBtn: 'Sign up',
continuewithgoogle: 'Continue with Google',
changeLanguageBtn: 'Change language',
search: 'Search',
login: 'Login',
@@ -189,6 +190,7 @@ export const fr: typeof en = {
welcomeMessage: 'Re-Bonjour ',
signOutBtn: 'Se déconnecter',
signInBtn: 'Se connecter',
continuewithgoogle: 'Continuer avec Google',
changeLanguageBtn: 'Changer la langue',
searchBtn: 'Rechercher',
playBtn: 'Jouer',
@@ -545,4 +547,5 @@ export const sp: typeof en = {
recentSearches: 'Búsquedas recientes',
noRecentSearches: 'No hay búsquedas recientes',
continuewithgoogle: 'Continuar con Google',
};

View File

@@ -5,8 +5,9 @@ import ResponseHandler from './ResponseHandler';
export const UserValidator = yup
.object({
username: yup.string().required(),
password: yup.string().required(),
password: yup.string().required().nullable(),
email: yup.string().required(),
googleID: yup.string().required().nullable(),
isGuest: yup.boolean().required(),
partyPlayed: yup.number().required(),
})
@@ -30,6 +31,7 @@ export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, U
interface User extends Model {
name: string;
email: string;
googleID: string | null;
isGuest: boolean;
premium: boolean;
data: UserData;

View File

@@ -22,4 +22,4 @@ server {
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
}
}
}

View File

@@ -8,6 +8,7 @@ import SigninForm from '../components/forms/signinform';
import SignupForm from '../components/forms/signupform';
import TextButton from '../components/TextButton';
import { RouteProps, useNavigation } from '../Navigation';
import * as Linking from 'expo-linking';
const hanldeSignin = async (
username: string,
@@ -56,6 +57,13 @@ const AuthenticationView = ({ isSignup }: RouteProps<AuthenticationViewProps>) =
<Text>
<Translate translationKey="welcome" />
</Text>
<TextButton
translate={{ translationKey: 'continuewithgoogle' }}
variant="outline"
marginTop={5}
colorScheme="primary"
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
/>
{mode === 'signin' ? (
<SigninForm
onSubmit={(username, password) =>

View File

@@ -0,0 +1,31 @@
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import API from '../API';
import { setAccessToken } from '../state/UserSlice';
import { Text } from 'native-base';
import { useRoute } from '@react-navigation/native';
import { AccessTokenResponseHandler } from '../models/AccessTokenResponse';
const GoogleView = () => {
const dispatch = useDispatch();
const route = useRoute();
useEffect(() => {
const params = route.path?.replace('/logged/google', '');
async function run() {
const accessToken = await API.fetch(
{
route: `/auth/logged/google${params}`,
method: 'GET',
},
{ handler: AccessTokenResponseHandler }
).then((responseBody) => responseBody.access_token);
dispatch(setAccessToken(accessToken));
}
run();
}, []);
return <Text>Loading please wait</Text>;
};
export default GoogleView;

View File

@@ -87,6 +87,17 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
text: user.id.toString(),
},
},
{
type: 'text',
title: 'Google Account',
data: {
text: user.googleID ? 'Linked' : 'Not linked',
},
// type: 'custom',
// data: user.googleID
// ? <Button><Text>Unlink</Text></Button>
// : <Button><Text>Link</Text></Button>,
},
{
type: 'text',
title: translate('nbGamesPlayed'),