3 Commits

Author SHA1 Message Date
Arthur Jamet
cec07b7e99 Front: Handle Error when guest username is already taken 2024-01-04 11:46:13 +01:00
Arthur Jamet
f93968c3eb Front: Add Username for Guest Mode 2024-01-04 11:33:11 +01:00
Arthur Jamet
f80253cea3 Back: Require Username for Guest Account Creation 2024-01-04 09:55:45 +01:00
10 changed files with 140 additions and 35 deletions

View File

@@ -51,6 +51,7 @@ import { PasswordResetDto } from "./dto/password_reset.dto ";
import { mapInclude } from "src/utils/include"; import { mapInclude } from "src/utils/include";
import { SongController } from "src/song/song.controller"; import { SongController } from "src/song/song.controller";
import { ChromaAuthGuard } from "./chroma-auth.guard"; import { ChromaAuthGuard } from "./chroma-auth.guard";
import { GuestDto } from "./dto/guest.dto";
@ApiTags("auth") @ApiTags("auth")
@Controller("auth") @Controller("auth")
@@ -162,8 +163,8 @@ export class AuthController {
@HttpCode(200) @HttpCode(200)
@ApiOperation({ description: "Login as a guest account" }) @ApiOperation({ description: "Login as a guest account" })
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken }) @ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
async guest(): Promise<JwtToken> { async guest(@Body() guestdto: GuestDto): Promise<JwtToken> {
const user = await this.usersService.createGuest(); const user = await this.usersService.createGuest(guestdto.username);
await this.settingsService.createUserSetting(user.id); await this.settingsService.createUserSetting(user.id);
return this.authService.login(user); return this.authService.login(user);
} }

View File

@@ -0,0 +1,8 @@
import { IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class GuestDto {
@ApiProperty()
@IsNotEmpty()
username: string;
}

View File

@@ -6,7 +6,7 @@ import {
import { User, Prisma } from "@prisma/client"; import { User, Prisma } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import * as bcrypt from "bcryptjs"; import * as bcrypt from "bcryptjs";
import { createHash, randomUUID } from "crypto"; import { createHash } from "crypto";
import { createReadStream, existsSync } from "fs"; import { createReadStream, existsSync } from "fs";
import fetch from "node-fetch"; import fetch from "node-fetch";
@@ -46,10 +46,10 @@ export class UsersService {
}); });
} }
async createGuest(): Promise<User> { async createGuest(displayName: string): Promise<User> {
return this.prisma.user.create({ return this.prisma.user.create({
data: { data: {
username: `Guest ${randomUUID()}`, username: displayName,
isGuest: true, isGuest: true,
// Not realyl clean but better than a separate table or breaking the api by adding nulls. // Not realyl clean but better than a separate table or breaking the api by adding nulls.
email: null, email: null,

View File

@@ -9,7 +9,7 @@ Resource ./auth.resource
*** Test Cases *** *** Test Cases ***
LoginAsGuest LoginAsGuest
[Documentation] Login as a guest [Documentation] Login as a guest
&{res}= POST /auth/guest &{res}= POST /auth/guest {"username": "i-am-a-guest"}
Output Output
Integer response status 200 Integer response status 200
String response body access_token String response body access_token
@@ -20,12 +20,13 @@ LoginAsGuest
Integer response status 200 Integer response status 200
Boolean response body isGuest true Boolean response body isGuest true
Integer response body partyPlayed 0 Integer response body partyPlayed 0
String response body username "i-am-a-guest"
[Teardown] DELETE /auth/me [Teardown] DELETE /auth/me
TwoGuests TwoGuests
[Documentation] Login as a guest [Documentation] Login as a guest
&{res}= POST /auth/guest &{res}= POST /auth/guest {"username": "i-am-another-guest"}
Output Output
Integer response status 200 Integer response status 200
String response body access_token String response body access_token
@@ -36,8 +37,9 @@ TwoGuests
Integer response status 200 Integer response status 200
Boolean response body isGuest true Boolean response body isGuest true
Integer response body partyPlayed 0 Integer response body partyPlayed 0
String response body username "i-am-another-guest"
&{res2}= POST /auth/guest &{res2}= POST /auth/guest {"username": "i-am-a-third-guest"}
Output Output
Integer response status 200 Integer response status 200
String response body access_token String response body access_token
@@ -48,6 +50,7 @@ TwoGuests
Integer response status 200 Integer response status 200
Boolean response body isGuest true Boolean response body isGuest true
Integer response body partyPlayed 0 Integer response body partyPlayed 0
String response body username "i-am-a-third-guest"
[Teardown] Run Keywords DELETE /auth/me [Teardown] Run Keywords DELETE /auth/me
... AND Set Headers {"Authorization": "Bearer ${res.body.access_token}"} ... AND Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
@@ -55,7 +58,7 @@ TwoGuests
GuestToNormal GuestToNormal
[Documentation] Login as a guest and convert to a normal account [Documentation] Login as a guest and convert to a normal account
&{res}= POST /auth/guest &{res}= POST /auth/guest {"username": "i-will-be-a-real-user"}
Output Output
Integer response status 200 Integer response status 200
String response body access_token String response body access_token
@@ -65,11 +68,13 @@ GuestToNormal
Output Output
Integer response status 200 Integer response status 200
Boolean response body isGuest true Boolean response body isGuest true
String response body username "i-will-be-a-real-user"
${res}= PUT /auth/me { "username": "toto", "password": "toto", "email": "awdaw@b.c"} ${res}= PUT /auth/me { "password": "toto", "email": "awdaw@b.c"}
Output Output
Integer response status 200 Integer response status 200
String response body username "toto" String response body username "toto"
Boolean response body isGuest false Boolean response body isGuest false
String response body username "i-will-be-a-real-user"
[Teardown] DELETE /auth/me [Teardown] DELETE /auth/me

View File

@@ -187,12 +187,12 @@ export default class API {
}); });
} }
public static async createAndGetGuestAccount(): Promise<AccessToken> { public static async createAndGetGuestAccount(username: string): Promise<AccessToken> {
return API.fetch( return API.fetch(
{ {
route: '/auth/guest', route: '/auth/guest',
method: 'POST', method: 'POST',
body: undefined, body: { username },
}, },
{ handler: AccessTokenResponseHandler } { handler: AccessTokenResponseHandler }
) )

View File

@@ -9,6 +9,7 @@ import SignUpForm from '../../components/forms/signupform';
import API, { APIError } from '../../API'; import API, { APIError } from '../../API';
import PopupCC from './PopupCC'; import PopupCC from './PopupCC';
import { StyleProp, ViewStyle } from 'react-native'; import { StyleProp, ViewStyle } from 'react-native';
import { useQuery } from '../../Queries';
const handleSubmit = async (username: string, password: string, email: string) => { const handleSubmit = async (username: string, password: string, email: string) => {
try { try {
@@ -36,6 +37,7 @@ const LogoutButtonCC = ({
}: LogoutButtonCCProps) => { }: LogoutButtonCCProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const user = useQuery(API.getUserInfo);
return ( return (
<> <>
@@ -54,7 +56,7 @@ const LogoutButtonCC = ({
isVisible={isVisible} isVisible={isVisible}
setIsVisible={setIsVisible} setIsVisible={setIsVisible}
> >
<SignUpForm onSubmit={handleSubmit} /> <SignUpForm onSubmit={handleSubmit} defaultValues={{ username: user.data?.name }} />
<ButtonBase <ButtonBase
style={!collapse ? { width: '100%' } : {}} style={!collapse ? { width: '100%' } : {}}
type="outlined" type="outlined"

View File

@@ -1,9 +1,9 @@
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { Stack, View, Text, Wrap, Image, Row, Column, ScrollView, useToast } from 'native-base'; import { Stack, View, Text, Wrap, Image, Row, Column, ScrollView } from 'native-base';
import { FunctionComponent } from 'react'; import { FunctionComponent, useState } from 'react';
import { Linking, Platform, useWindowDimensions } from 'react-native'; import { Linking, Platform, useWindowDimensions } from 'react-native';
import ButtonBase from './ButtonBase'; import ButtonBase from './ButtonBase';
import { translate } from '../../i18n/i18n'; import { Translate, translate } from '../../i18n/i18n';
import API, { APIError } from '../../API'; import API, { APIError } from '../../API';
import SeparatorBase from './SeparatorBase'; import SeparatorBase from './SeparatorBase';
import LinkBase from './LinkBase'; import LinkBase from './LinkBase';
@@ -12,9 +12,14 @@ import { setAccessToken } from '../../state/UserSlice';
import useColorScheme from '../../hooks/colorScheme'; import useColorScheme from '../../hooks/colorScheme';
import { useAssets } from 'expo-asset'; import { useAssets } from 'expo-asset';
import APKDownloadButton from '../APKDownloadButton'; import APKDownloadButton from '../APKDownloadButton';
import PopupCC from './PopupCC';
import GuestForm from '../forms/guestForm';
const handleGuestLogin = async (apiSetter: (accessToken: string) => void): Promise<string> => { const handleGuestLogin = async (
const apiAccess = await API.createAndGetGuestAccount(); username: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
const apiAccess = await API.createAndGetGuestAccount(username);
apiSetter(apiAccess); apiSetter(apiAccess);
return translate('loggedIn'); return translate('loggedIn');
}; };
@@ -36,8 +41,8 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
}) => { }) => {
const layout = useWindowDimensions(); const layout = useWindowDimensions();
const dispatch = useDispatch(); const dispatch = useDispatch();
const toast = useToast();
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const [guestModalIsOpen, openGuestModal] = useState(false);
const [logo] = useAssets( const [logo] = useAssets(
colorScheme == 'light' colorScheme == 'light'
? require('../../assets/icon_light.png') ? require('../../assets/icon_light.png')
@@ -85,21 +90,32 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
</Row> </Row>
<ButtonBase <ButtonBase
title={translate('guestMode')} title={translate('guestMode')}
onPress={async () => { onPress={() => openGuestModal(true)}
try {
handleGuestLogin((accessToken: string) => {
dispatch(setAccessToken(accessToken));
});
} catch (error) {
if (error instanceof APIError) {
toast.show({ description: translate(error.userMessage) });
return;
}
toast.show({ description: error as string });
}
}}
/> />
</Wrap> </Wrap>
<PopupCC
title={translate('guestMode')}
isVisible={guestModalIsOpen}
setIsVisible={openGuestModal}
>
<GuestForm
onSubmit={(username) =>
handleGuestLogin(username, (accessToken: string) => {
dispatch(setAccessToken(accessToken));
})
.then(() => {
openGuestModal(false);
return translate('loggedIn');
})
.catch((error) => {
if (error instanceof APIError) {
return translate('usernameTaken');
}
return error as string;
})
}
/>
</PopupCC>
<ScrollView <ScrollView
contentContainerStyle={{ contentContainerStyle={{
padding: 16, padding: 16,
@@ -152,7 +168,9 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
title={translate('continuewithgoogle')} title={translate('continuewithgoogle')}
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)} onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
/> />
<SeparatorBase>or</SeparatorBase> <SeparatorBase>
<Translate translationKey="or" />
</SeparatorBase>
<Stack <Stack
space={3} space={3}
justifyContent="center" justifyContent="center"

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { translate } from '../../i18n/i18n';
import { string } from 'yup';
import { useToast, Column } from 'native-base';
import TextFormField from '../UI/TextFormField';
import ButtonBase from '../UI/ButtonBase';
import { Sms } from 'iconsax-react-native';
import { useWindowDimensions } from 'react-native';
interface GuestFormProps {
onSubmit: (username: string) => Promise<string>;
}
const validationSchemas = {
username: string().required('Username is required'),
};
const GuestForm = ({ onSubmit }: GuestFormProps) => {
const [formData, setFormData] = React.useState({
username: {
value: '',
error: null as string | null,
},
});
const toast = useToast();
const layout = useWindowDimensions();
return (
<Column style={{ width: layout.width * 0.5 }}>
<TextFormField
style={{ marginVertical: 10 }}
isRequired
icon={Sms}
placeholder={translate('username')}
value={formData.username.value}
error={formData.username.error}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.username
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
/>
<ButtonBase
isDisabled={formData.username.error !== null || formData.username.value === ''}
type={'filled'}
title={translate('submitBtn')}
style={{ marginVertical: 10 }}
onPress={() => {
onSubmit(formData.username.value)
.then((e) => {
toast.show({ description: e as string });
})
.catch((e) => {
toast.show({ description: e as string });
});
}}
/>
</Column>
);
};
export default GuestForm;

View File

@@ -10,13 +10,14 @@ import ButtonBase from '../UI/ButtonBase';
import Spacer from '../UI/Spacer'; import Spacer from '../UI/Spacer';
interface SignupFormProps { interface SignupFormProps {
defaultValues: Partial<{ username: string }>;
onSubmit: (username: string, password: string, email: string) => Promise<string>; onSubmit: (username: string, password: string, email: string) => Promise<string>;
} }
const SignUpForm = ({ onSubmit }: SignupFormProps) => { const SignUpForm = ({ onSubmit, defaultValues }: SignupFormProps) => {
const [formData, setFormData] = React.useState({ const [formData, setFormData] = React.useState({
username: { username: {
value: '', value: defaultValues.username || '',
error: null as string | null, error: null as string | null,
}, },
password: { password: {

View File

@@ -1,5 +1,6 @@
export const en = { export const en = {
error: 'Error', error: 'Error',
or: 'or',
guestMode: 'Guest Mode', guestMode: 'Guest Mode',
downloadAPK: 'Download Android App', downloadAPK: 'Download Android App',
goBackHome: 'Go Back Home', goBackHome: 'Go Back Home',
@@ -323,6 +324,7 @@ export const en = {
export const fr: typeof en = { export const fr: typeof en = {
error: 'Erreur', error: 'Erreur',
or: 'ou',
downloadAPK: "Télécharger l'App Android", downloadAPK: "Télécharger l'App Android",
guestMode: 'Mode Invité', guestMode: 'Mode Invité',
goBackHome: "Retourner à l'accueil", goBackHome: "Retourner à l'accueil",
@@ -646,6 +648,7 @@ export const fr: typeof en = {
export const sp: typeof en = { export const sp: typeof en = {
error: 'Error', error: 'Error',
or: 'u',
downloadAPK: 'Descarga la Aplicación de Android', downloadAPK: 'Descarga la Aplicación de Android',
guestMode: 'Modo Invitado', guestMode: 'Modo Invitado',
anErrorOccured: 'ocurrió un error', anErrorOccured: 'ocurrió un error',