Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,9 @@ To run the application from source, follow these steps:
1. Open the `src/Turnierplan.sln` solution and navigate to the docker compose file located under `Solution Items`. Run the `turnierplan.database` docker compose service. This will start up the PostgreSQL database for local development.
2. Navigate to the `Turnierplan.App` project and run the `Turnierplan.App` launch configuration. This will start the backend using port `45000`.
3. Open a terminal and navigate to the `src/Turnierplan.App/Client` directory. Run `npm install` to install the node dependencies. Next, you can start the client application by typing `npm run start`. Note that this will only work if the backend application has previously been run because the client app startup depends on OpenAPI files generated by the backend build process.
4. Access the client application using [http://localhost:45001](http://localhost:45001) and log in using default credentials. The email address is `admin@example.com` and the password is `P@ssw0rd`.
4. Access the client application using [http://localhost:45001](http://localhost:45001) and log in using default credentials. The user name is `admin` and the password is `P@ssw0rd`.

When running locally, the API documentation can be viewed by opening [http://localhost:45000/scalar]( http://localhost:45000/scalar).
When running locally, the API documentation can be viewed by opening [http://localhost:45000/scalar](http://localhost:45000/scalar).

## Screenshots

Expand Down
4 changes: 2 additions & 2 deletions src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This docker compose file is used for running the E2E tests during the CI pipeline.
# This docker compose file is used for running the E2E tests during the CI workflow

services:
turnierplan.e2e.database:
Expand All @@ -19,7 +19,7 @@ services:
- "45001:8080"
environment:
- Database__ConnectionString=Host=turnierplan.e2e.database;Database=turnierplan;Username=postgres;Password=P@ssw0rd
- Turnierplan__ApplicationUrl=http://localhost:45009
- Turnierplan__ApplicationUrl=http://localhost:45001
- Turnierplan__InitialUserPassword=P@ssw0rd

networks:
Expand Down
2 changes: 1 addition & 1 deletion src/Turnierplan.App/Client/cypress/support/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Cypress.Commands.add('getx', (id) => {
Cypress.Commands.add('login', () => {
cy.visit('/portal/login');

cy.getx(turnierplan.loginPage.emailField).type('admin@example.com');
cy.getx(turnierplan.loginPage.userNameField).type('admin');
cy.getx(turnierplan.loginPage.passwordField).type('P@ssw0rd');
cy.getx(turnierplan.loginPage.loginButton).click();
});
Expand Down
2 changes: 1 addition & 1 deletion src/Turnierplan.App/Client/cypress/support/turnierplan.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const turnierplan = {
newOrganizationButton: 'landing-page-new-organization-button'
},
loginPage: {
emailField: 'login-page-email-field',
userNameField: 'login-page-user-name-field',
loginButton: 'login-page-login-button',
passwordField: 'login-page-password-field'
},
Expand Down
5 changes: 3 additions & 2 deletions src/Turnierplan.App/Client/src/app/core/models/identity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface AuthenticatedUser {
id: string;
displayName: string;
emailAddress: string;
userName: string;
fullName?: string;
emailAddress?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { TurnierplanApi } from '../../api/turnierplan-api';
import { login } from '../../api/fn/identity/login';
import { NullableOfChangePasswordFailedReason } from '../../api/models/nullable-of-change-password-failed-reason';
import { changePassword } from '../../api/fn/identity/change-password';
import { updateUserData } from '../../api/fn/identity/update-user-data';
import { refresh } from '../../api/fn/identity/refresh';
import { logout } from '../../api/fn/identity/logout';

interface TurnierplanAccessToken {
exp: number;
mail: string;
name: string;
userName: string;
fullName: string;
adm?: string;
uid: string;
}
Expand All @@ -29,11 +29,22 @@ interface TurnierplanRefreshToken {
export class AuthenticationService implements OnDestroy {
private static readonly localStorageUserIdKey = 'tp_id_userId';
private static readonly localStorageUserNameKey = 'tp_id_userName';
private static readonly localStorageUserFullNameKey = 'tp_id_userFullName';
private static readonly localStorageUserEMailKey = 'tp_id_userEMail';
private static readonly localStorageUserAdministratorKey = 'tp_id_userAdmin';
private static readonly localStorageAccessTokenExpiryKey = 'tp_id_accTokenExp';
private static readonly localStorageRefreshTokenExpiryKey = 'tp_id_rfsTokenExp';

private static readonly allLocalStorageKeys = [
AuthenticationService.localStorageUserIdKey,
AuthenticationService.localStorageUserNameKey,
AuthenticationService.localStorageUserFullNameKey,
AuthenticationService.localStorageUserEMailKey,
AuthenticationService.localStorageUserAdministratorKey,
AuthenticationService.localStorageAccessTokenExpiryKey,
AuthenticationService.localStorageRefreshTokenExpiryKey
];

// Clock skew should be as short as possible, but still long enough that a request to the token
// refresh endpoint can complete even in the case of a bad connection or unfavorable conditions.
private static readonly tokenExpiryCheckClockSkewSeconds = 10;
Expand All @@ -50,10 +61,16 @@ export class AuthenticationService implements OnDestroy {
) {
const storedUserId = this.readUserIdFromLocalStorage();
const storedUserName = this.readUserNameFromLocalStorage();
const storedUserFullName = this.readUserFullNameFromLocalStorage();
const storedUserEMail = this.readUserEMailFromLocalStorage();

if (storedUserId && storedUserName && storedUserEMail) {
this.authentication$.next({ id: storedUserId, displayName: storedUserName, emailAddress: storedUserEMail });
if (storedUserId && storedUserName) {
this.authentication$.next({
id: storedUserId,
userName: storedUserName,
fullName: storedUserFullName,
emailAddress: storedUserEMail
});
}
}

Expand All @@ -62,8 +79,8 @@ export class AuthenticationService implements OnDestroy {
this.destroyed$.complete();
}

public login(email: string, password: string): Observable<'success' | 'failure'> {
return this.turnierplanApi.invoke(login, { body: { eMail: email, password: password } }).pipe(
public login(userName: string, password: string): Observable<'success' | 'failure'> {
return this.turnierplanApi.invoke(login, { body: { userName: userName, password: password } }).pipe(
catchError(() => of(undefined)),
map((result) => {
if (result && result.success && result.accessToken && result.refreshToken) {
Expand All @@ -72,7 +89,8 @@ export class AuthenticationService implements OnDestroy {

this.updateLocalStorageCache(
decodedAccessToken.uid,
decodedAccessToken.name,
decodedAccessToken.userName,
decodedAccessToken.fullName,
decodedAccessToken.mail,
decodedAccessToken.adm === 'true',
decodedAccessToken.exp,
Expand All @@ -81,7 +99,8 @@ export class AuthenticationService implements OnDestroy {

this.authentication$.next({
id: decodedAccessToken.uid,
displayName: decodedAccessToken.name,
userName: decodedAccessToken.userName,
fullName: decodedAccessToken.fullName,
emailAddress: decodedAccessToken.mail
});

Expand All @@ -97,10 +116,6 @@ export class AuthenticationService implements OnDestroy {
this.logoutAndClearData(() => window.location.assign('/')).subscribe();
}

public openEditUserInfoForm(): void {
void this.router.navigate(['portal/user-info'], { queryParams: { redirect_to: this.router.url } });
}

public openChangePasswordForm(): void {
void this.router.navigate(['portal/change-password'], { queryParams: { redirect_to: this.router.url } });
}
Expand Down Expand Up @@ -132,14 +147,14 @@ export class AuthenticationService implements OnDestroy {
}

public changePassword(
userEmail: string,
userName: string,
newPassword: string,
currentPassword: string
): Observable<'success' | 'failure' | NullableOfChangePasswordFailedReason> {
return this.turnierplanApi
.invoke(changePassword, {
body: {
eMail: userEmail,
userName: userName,
newPassword: newPassword,
currentPassword: currentPassword
}
Expand All @@ -163,22 +178,6 @@ export class AuthenticationService implements OnDestroy {
);
}

public changeUserInformation(userName: string, emailAddress: string): Observable<'success' | 'emailVerificationPending' | 'failure'> {
return this.turnierplanApi.invoke(updateUserData, { body: { userName: userName, eMail: emailAddress } }).pipe(
catchError(() => of(undefined)),
map((result) => {
if (result?.success !== true) {
return 'failure';
}

this.updateLocalStorageCacheUserInformationOnly(userName, emailAddress);
this.authentication$.next({ id: this.readUserIdFromLocalStorage()!, displayName: userName, emailAddress: emailAddress });

return 'success';
})
);
}

private ensureAccessTokenUnprotected(): Observable<boolean> {
const accessTokenExpiry = this.readAccessTokenExpiryFromLocalStorage();

Expand All @@ -189,9 +188,12 @@ export class AuthenticationService implements OnDestroy {
}

const logoutWithRedirect = (): Observable<boolean> => {
// Store userName in variable because logoutAndClearData() removes it from local storage
const userName = this.readUserNameFromLocalStorage();

return this.logoutAndClearData(() => {
void this.router.navigate(['/portal/login'], {
queryParams: { redirect_to: this.router.url, email: this.readUserEMailFromLocalStorage() }
queryParams: { redirect_to: this.router.url, user_name: userName }
});
}).pipe(map(() => false));
};
Expand All @@ -212,7 +214,8 @@ export class AuthenticationService implements OnDestroy {

this.updateLocalStorageCache(
decodedAccessToken.uid,
decodedAccessToken.name,
decodedAccessToken.userName,
decodedAccessToken.fullName,
decodedAccessToken.mail,
decodedAccessToken.adm === 'true',
decodedAccessToken.exp,
Expand All @@ -221,7 +224,8 @@ export class AuthenticationService implements OnDestroy {

this.authentication$.next({
id: decodedAccessToken.uid,
displayName: decodedAccessToken.name,
userName: decodedAccessToken.userName,
fullName: decodedAccessToken.fullName,
emailAddress: decodedAccessToken.mail
});

Expand Down Expand Up @@ -269,6 +273,10 @@ export class AuthenticationService implements OnDestroy {
return localStorage.getItem(AuthenticationService.localStorageUserNameKey) ?? undefined;
}

private readUserFullNameFromLocalStorage(): string | undefined {
return localStorage.getItem(AuthenticationService.localStorageUserFullNameKey) ?? undefined;
}

private readUserEMailFromLocalStorage(): string | undefined {
return localStorage.getItem(AuthenticationService.localStorageUserEMailKey) ?? undefined;
}
Expand All @@ -292,38 +300,30 @@ export class AuthenticationService implements OnDestroy {
private updateLocalStorageCache(
userId: string,
userName: string,
userEMail: string,
userFullName: string | undefined,
userEMail: string | undefined,
userIsAdmin: boolean,
accessTokenExpiry: number,
refreshTokenExpiry: number
): void {
localStorage.setItem(AuthenticationService.localStorageUserIdKey, userId);
localStorage.setItem(AuthenticationService.localStorageUserNameKey, userName);
localStorage.setItem(AuthenticationService.localStorageUserEMailKey, userEMail);
localStorage.setItem(AuthenticationService.localStorageUserFullNameKey, userFullName ?? '');
localStorage.setItem(AuthenticationService.localStorageUserEMailKey, userEMail ?? '');
localStorage.setItem(AuthenticationService.localStorageUserAdministratorKey, `${userIsAdmin}`);

localStorage.setItem(AuthenticationService.localStorageAccessTokenExpiryKey, `${accessTokenExpiry}`);
localStorage.setItem(AuthenticationService.localStorageRefreshTokenExpiryKey, `${refreshTokenExpiry}`);
}

private updateLocalStorageCacheUserInformationOnly(userName: string, userEMail: string): void {
localStorage.setItem(AuthenticationService.localStorageUserNameKey, userName);
localStorage.setItem(AuthenticationService.localStorageUserEMailKey, userEMail);
}

private updateLocalStorageCacheRefreshTokenExpiryOnly(refreshTokenExpiry: number): void {
localStorage.setItem(AuthenticationService.localStorageRefreshTokenExpiryKey, `${refreshTokenExpiry}`);
}

private logoutAndClearData(navigateTo?: () => void): Observable<void> {
const logout$ = this.turnierplanApi.invoke(logout).pipe(
catchError(() => of(undefined)),
tap(() => {
localStorage.removeItem(AuthenticationService.localStorageUserNameKey);
localStorage.removeItem(AuthenticationService.localStorageUserEMailKey);
localStorage.removeItem(AuthenticationService.localStorageAccessTokenExpiryKey);
localStorage.removeItem(AuthenticationService.localStorageRefreshTokenExpiryKey);
}),
tap(() => AuthenticationService.allLocalStorageKeys.forEach((key) => localStorage.removeItem(key))),
map(() => void 0)
);

Expand Down
43 changes: 13 additions & 30 deletions src/Turnierplan.App/Client/src/app/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const de = {
EmptyOrExtraWhitespace: 'Geben Sie ein neues Passwort an, welches keine Leerzeichen am Anfang oder Ende hat.',
PasswordsDoNotMatch: 'Die beiden Passwörter stimmen nicht überein.',
InsecurePassword:
'Das neue Passwort muss folgende Kriterien erfüllen:<ul><li>min. 10 Zeichen lang</li><li>min. 1 Großbuchstabe</li><li>min. 1 Kleinbuchstabe</li><li>min. 1 Ziffer</li><li>min. 1 Sonderzeichen</li></ul>',
'Das neue Passwort muss folgende Kriterien erfüllen:<ul><li>min. 8 Zeichen lang</li><li>min. 1 Buchstabe</li><li>min. 1 Ziffer</li><li>min. 1 Sonderzeichen</li></ul>',
InvalidCredentials: 'Das aktuelle Passwort ist falsch.',
NewPasswordEqualsCurrent: 'Das neue Passwort darf nicht dem bisherigen Passwort entsprechen.',
UnexpectedError: 'Bei der Bearbeitung der Anfrage ist ein Fehler aufgetreten.'
Expand All @@ -37,27 +37,9 @@ export const de = {
Back: 'Abbrechen',
Submit: 'Speichern'
},
ChangeUserInfo: {
Title: 'Benutzerprofil',
UserName: 'Nutzername:',
EMail: 'E-Mail Adresse:',
ChangeEmailNotice: 'Wenn Sie Ihre E-Mail Adresse ändern, müssen Sie sich ab sofort mit dieser neuen E-Mail Adresse anmelden.',
RequestFailed: 'Bei der Bearbeitung der Anfrage ist ein Fehler aufgetreten.',
SuccessToast: {
Title: 'Nutzerdaten aktualisiert',
Message: 'Ihr Benutzerinformationen wurden gespeichert.'
},
EmailVerificationPendingToast: {
Title: 'E-Mail muss bestätigt werden',
Message:
'Ihr Benutzerinformationen wurden gespeichert. Klicken Sie auf den Link in der Mail, welche wir Ihnen geschickt haben, um die neue E-Mail zu bestätigen.'
},
Back: 'Abbrechen',
Submit: 'Speichern'
},
Login: {
Title: 'Anmelden',
EMail: 'E-Mail Adresse:',
UserName: 'Benutzername:',
Password: 'Passwort:',
CookieNotice: 'Wenn Sie sich anmelden, wird ihre aktive Sitzung in einem Cookie gespeichert.',
Submit: 'Anmelden',
Expand Down Expand Up @@ -85,7 +67,6 @@ export const de = {
UserInfoPopover: {
Text: 'Sie sind angemeldet als:\n<strong>{{userName}}</strong>',
Administration: 'Administration',
EditUserInfo: 'Benutzerinformation',
ChangePassword: 'Passwort ändern',
Logout: 'Abmelden'
},
Expand Down Expand Up @@ -117,8 +98,8 @@ export const de = {
NewUser: 'Neuer Benutzer',
Users: {
TableLabel: 'Benutzer in dieser turnierplan.NET-Instanz',
Id: 'ID',
Name: 'Name',
UserName: 'Benutzername',
FullName: 'Name',
EMail: 'E-Mail',
CreatedAt: 'Erstellt am',
LastPasswordChange: 'Letzte Passwortänderung',
Expand All @@ -127,8 +108,9 @@ export const de = {
EditUser: {
Title: 'Benutzer bearbeiten',
Info: 'Ändern Sie die Informationen eines bestehenden Benutzers. Beachten Sie, dass der betroffene Nutzer diese Änderungen unter Umständen nicht direkt sieht.',
Name: 'Name:',
NameInvalid: 'Der Name eines neuen Nutzers darf nicht leer sein.',
UserName: 'Benutzername:',
UserNameInvalid: 'Der Benutzername eines Nutzers darf nicht leer sein.',
FullName: 'Name:',
Email: 'E-Mailadresse',
EmailInvalid: 'Die eingegebene E-Mailadresse ist ungültig.',
IsAdministrator: 'Administrator',
Expand All @@ -155,14 +137,15 @@ export const de = {
Title: 'Neuen Benutzer',
LongTitle: 'Neuen Benutzer erstellen',
Form: {
UserName: 'Name',
UserName: 'Benutzername:',
UserNameInvalid: 'Der Name eines neuen Nutzers darf nicht leer sein.',
Email: 'E-Mailadresse',
FullName: 'Name:',
Email: 'E-Mailadresse:',
EmailInvalid: 'Die eingegebene E-Mailadresse ist ungültig.',
Password: 'Passwort',
Password: 'Passwort:',
PasswordInvalid: 'Das eingegebene Passwort ist ungültig.'
},
UserNotice: 'Der erstellte Nutzer kann sich unmittelbar danach mit E-Mail und Passwort anmelden.',
UserNotice: 'Der erstellte Nutzer kann sich unmittelbar danach mit Benutzername und Passwort anmelden.',
Submit: 'Erstellen'
},
CreateOrganization: {
Expand Down Expand Up @@ -1247,7 +1230,7 @@ export const de = {
SelectedRole: 'Gewählte Rolle:',
SearchPrincipalPlaceholder: {
ApiKey: 'ID des API-Schlüssels eingeben',
User: 'E-Mailadresse des Nutzers eingeben'
User: 'Benutzername oder E-Mailadresse des Nutzers eingeben'
},
SearchPrincipalButton: 'Suchen & hinzufügen',
CreatingRoleAssignment: 'Die Rollenzuweisung wird erstellt',
Expand Down
Loading
Loading