Compare commits
3 Commits
main
...
guest-user
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cec07b7e99 | ||
|
|
f93968c3eb | ||
|
|
f80253cea3 |
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
8
back/src/auth/dto/guest.dto.ts
Normal file
8
back/src/auth/dto/guest.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class GuestDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNotEmpty()
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
67
front/components/forms/guestForm.tsx
Normal file
67
front/components/forms/guestForm.tsx
Normal 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;
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user