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 { SongController } from "src/song/song.controller";
import { ChromaAuthGuard } from "./chroma-auth.guard";
import { GuestDto } from "./dto/guest.dto";
@ApiTags("auth")
@Controller("auth")
@@ -162,8 +163,8 @@ export class AuthController {
@HttpCode(200)
@ApiOperation({ description: "Login as a guest account" })
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
async guest(): Promise<JwtToken> {
const user = await this.usersService.createGuest();
async guest(@Body() guestdto: GuestDto): Promise<JwtToken> {
const user = await this.usersService.createGuest(guestdto.username);
await this.settingsService.createUserSetting(user.id);
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 { PrismaService } from "src/prisma/prisma.service";
import * as bcrypt from "bcryptjs";
import { createHash, randomUUID } from "crypto";
import { createHash } from "crypto";
import { createReadStream, existsSync } from "fs";
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({
data: {
username: `Guest ${randomUUID()}`,
username: displayName,
isGuest: true,
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
email: null,

View File

@@ -9,7 +9,7 @@ Resource ./auth.resource
*** Test Cases ***
LoginAsGuest
[Documentation] Login as a guest
&{res}= POST /auth/guest
&{res}= POST /auth/guest {"username": "i-am-a-guest"}
Output
Integer response status 200
String response body access_token
@@ -20,12 +20,13 @@ LoginAsGuest
Integer response status 200
Boolean response body isGuest true
Integer response body partyPlayed 0
String response body username "i-am-a-guest"
[Teardown] DELETE /auth/me
TwoGuests
[Documentation] Login as a guest
&{res}= POST /auth/guest
&{res}= POST /auth/guest {"username": "i-am-another-guest"}
Output
Integer response status 200
String response body access_token
@@ -36,8 +37,9 @@ TwoGuests
Integer response status 200
Boolean response body isGuest true
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
Integer response status 200
String response body access_token
@@ -48,6 +50,7 @@ TwoGuests
Integer response status 200
Boolean response body isGuest true
Integer response body partyPlayed 0
String response body username "i-am-a-third-guest"
[Teardown] Run Keywords DELETE /auth/me
... AND Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
@@ -55,7 +58,7 @@ TwoGuests
GuestToNormal
[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
Integer response status 200
String response body access_token
@@ -65,11 +68,13 @@ GuestToNormal
Output
Integer response status 200
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
Integer response status 200
String response body username "toto"
Boolean response body isGuest false
String response body username "i-will-be-a-real-user"
[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(
{
route: '/auth/guest',
method: 'POST',
body: undefined,
body: { username },
},
{ handler: AccessTokenResponseHandler }
)

View File

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

View File

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

View File

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