Skip to content
Snippets Groups Projects
Commit b69d2c35 authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Resolve "Handle invitation links"

parent b5b61432
No related branches found
No related tags found
1 merge request!118Resolve "Handle invitation links"
......@@ -20,5 +20,9 @@ export type UserOutExtended = {
* Lifesicence ID of the user
*/
lifescience_id?: (string | null);
/**
* Timestamp when the invitation token was created as UNIX timestamp
*/
invitation_token_created_at?: (number | null);
};
......@@ -8,14 +8,16 @@ import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class AuthService {
/**
* Redirect to LifeScience OIDC Login
* Kickstart the login flow
* Redirect route to OIDC provider to kickstart the login process.
* @param invitationToken Unique token to validate an invitation
* @param provider The OIDC provider to use for login
* @param next Will be appended to redirect response in the callback route as URL query parameter `next_path`
* @param next Will be appended to redirect response in the callback route as URL query parameter `next`
* @returns void
* @throws ApiError
*/
public static authLogin(
invitationToken?: string,
provider?: OIDCProvider,
next?: string,
): CancelablePromise<void> {
......@@ -23,6 +25,7 @@ export class AuthService {
method: 'GET',
url: '/auth/login',
query: {
'invitation_token': invitationToken,
'provider': provider,
'next': next,
},
......@@ -65,6 +68,7 @@ export class AuthService {
}
/**
* Logout
* Logout the user from the system by deleting the bearer cookie.
* @returns void
* @throws ApiError
*/
......
......@@ -13,7 +13,7 @@ import { request as __request } from '../core/request';
export class UserService {
/**
* Create User
* Create a new user in the system and notify him. The smtp MUST be the same as the one saved by the OIDC provider.
* Create a new user in the system and notify him.
*
* Permission `user:create` required.
* @param requestBody
......@@ -172,4 +172,31 @@ export class UserService {
},
});
}
/**
* Resend Invitation
* Resend the invitation link for an user that has an open invitation.
*
* Permission `user:create` required.
* @param uid UID of a user
* @returns UserOutExtended Successful Response
* @throws ApiError
*/
public static userResendInvitation(
uid: string,
): CancelablePromise<UserOutExtended> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/users/{uid}/invitation',
path: {
'uid': uid,
},
errors: {
400: `Error decoding JWT Token`,
401: `Not Authenticated`,
403: `Not Authorized`,
404: `Entity not Found`,
422: `Validation Error`,
},
});
}
}
......@@ -45,7 +45,7 @@ function createUser() {
formState.loading = true;
formState.registeredUserName = formState.user.display_name;
userRepository
.createUser(formState.user)
.inviteUser(formState.user)
.then((user) => {
emit("user-created", user);
formState.validated = false;
......@@ -76,11 +76,11 @@ onMounted(() => {
<template>
<bootstrap-toast toast-id="create-user-success-toast">
Successfully registered user {{ formState.registeredUserName }}
Successfully invited user {{ formState.registeredUserName }}
</bootstrap-toast>
<bootstrap-toast toast-id="create-user-error-toast" color-class="danger">
<template #default
>Couldn't regsiter user
>Couldn't invite user
{{ formState.registeredUserName }}
</template>
<template #body>Error: {{ formState.errorMessage }}</template>
......@@ -90,7 +90,7 @@ onMounted(() => {
:static-backdrop="true"
modal-label="Create user"
>
<template #header>Register a user</template>
<template #header>Invite a user</template>
<template #body>
<form
id="create-user-form"
......
......@@ -31,8 +31,9 @@ const router = createRouter({
title: "Login",
},
props: (route) => ({
returnPath: route.query.return_path ?? undefined,
returnPath: route.query.next ?? undefined,
loginError: route.query.login_error ?? undefined,
invitationToken: route.query.invitation_token ?? undefined,
}),
},
{
......
......@@ -113,12 +113,18 @@ export const useUserStore = defineStore({
roles: roles,
});
},
createUser(userIn: UserIn): Promise<UserOutExtended> {
inviteUser(userIn: UserIn): Promise<UserOutExtended> {
return UserService.userCreateUser(userIn).then((user) => {
useNameStore().addNameToMapping(user.uid, user.display_name);
return user;
});
},
resendInvitationEmail(uid: string): Promise<UserOutExtended> {
return UserService.userResendInvitation(uid).then((user) => {
useNameStore().addNameToMapping(user.uid, user.display_name);
return user;
});
},
searchUser(searchString: string): Promise<UserOut[]> {
return UserService.userSearchUsers(searchString).then((users) => {
const nameStore = useNameStore();
......
......@@ -11,6 +11,7 @@ const router = useRouter();
const props = defineProps<{
returnPath?: string;
loginError?: string;
invitationToken?: string;
}>();
const store = useUserStore();
......@@ -24,9 +25,17 @@ onBeforeMount(() => {
}
});
const returnPathQuery = computed<string>(() =>
props.returnPath ? `&next=${encodeURI(props.returnPath)}` : "",
);
const loginPath = computed<string>(() => {
const loginUrl = new URL(`${OpenAPI.BASE}/auth/login?provider=lifescience`);
console.log(props);
if (props.returnPath) {
loginUrl.searchParams.append("next", encodeURI(props.returnPath));
}
if (props.invitationToken) {
loginUrl.searchParams.append("invitation_token", props.invitationToken);
}
return loginUrl.href;
});
onMounted(() => {
errorToast = new Toast("#loginErrorToast");
......@@ -69,15 +78,16 @@ onMounted(() => {
<div
class="card text-center ms-md-auto position-fixed top-50 start-50 translate-middle shadow"
>
<div class="card-header">Login</div>
<div v-if="invitationToken" class="card-header">Invitation</div>
<div v-else class="card-header">Login</div>
<div class="card-body">
<p class="card-text text-secondary">
<p v-if="invitationToken" class="card-text text-secondary">
Connect your newly created CloWM account with your LifeScience account
</p>
<p v-else class="card-text text-secondary">
Login to this service with LifeScience
</p>
<a
:href="`${OpenAPI.BASE}/auth/login?provider=lifescience${returnPathQuery}`"
class="m-2"
>
<a :href="loginPath" class="m-2">
<img src="/src/assets/images/ls-login.png" alt="[LS Login]" />
</a>
</div>
......
......@@ -9,7 +9,8 @@ import CreateUserModal from "@/components/admin/CreateUserModal.vue";
const userRepository = useUserStore();
type RoleList = RoleEnum[];
let successToast: Toast | undefined;
let successRoleToast: Toast | undefined;
let successInvitationToast: Toast | undefined;
let errorToast: Toast | undefined;
const userState = reactive<{
......@@ -62,7 +63,7 @@ function saveUserRoles(index: number) {
.then((user) => {
userState.users[index] = user;
userState.editUserRoles[index] = false;
successToast?.show();
successRoleToast?.show();
})
.catch((err) => {
userState.errorMessage = err.body?.detail;
......@@ -80,13 +81,29 @@ function resetUserRoles(index: number) {
);
}
function resendInvitationEmail(uid: string) {
userState.loading = true;
userRepository
.resendInvitationEmail(uid)
.then(() => {
successInvitationToast?.show();
})
.finally(() => {
userState.loading = false;
});
}
onMounted(() => {
successToast = new Toast("#change-role-success-toast");
successRoleToast = new Toast("#change-role-success-toast");
successInvitationToast = new Toast("#resend-invitation-success-toast");
errorToast = new Toast("#change-role-error-toast");
});
</script>
<template>
<bootstrap-toast toast-id="resend-invitation-success-toast">
Successfully resend invitation email
</bootstrap-toast>
<bootstrap-toast toast-id="change-role-success-toast">
Successfully change roles of user {{ userState.editedUser?.display_name }}
</bootstrap-toast>
......@@ -108,7 +125,7 @@ onMounted(() => {
data-bs-toggle="modal"
data-bs-target="#create-user-modal"
>
Register User
Invite User
</button>
</div>
<form @submit.prevent="searchUsers" id="admin-user-search-form">
......@@ -234,13 +251,35 @@ onMounted(() => {
</div>
</td>
<td class="text-end" v-else>
<button
type="button"
class="btn btn-sm btn-secondary"
@click="userState.editUserRoles[index] = true"
>
Edit Roles
</button>
<div class="btn-group">
<button
type="button"
class="btn btn-sm btn-secondary"
@click="userState.editUserRoles[index] = true"
>
Edit Roles
</button>
<button
v-if="user.invitation_token_created_at"
type="button"
class="btn btn-secondary btn-sm dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li>
<a
class="dropdown-item"
:class="{ disabled: userState.loading }"
:aria-disabled="userState.loading"
@click="resendInvitationEmail(user.uid)"
>Resend invitation email</a
>
</li>
</ul>
</div>
</td>
</tr>
</tbody>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment