Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3aab8e4
Added modal for use when changing the logged in user's password
Darkdusk234 Mar 2, 2026
726aed3
Added a route to get the currentUser with their stored hashed passwor…
Darkdusk234 Mar 3, 2026
27f95f6
Added role to userdata that is gotten
Darkdusk234 Mar 3, 2026
4eee5cb
Updated route with GetCurrentUserRequest interface
Darkdusk234 Mar 3, 2026
815ccba
Removed input from getCurrentUser method
Darkdusk234 Mar 3, 2026
c82a177
Added new route to use when updating a users password
Darkdusk234 Mar 3, 2026
5acd2b9
added changePassword method
Darkdusk234 Mar 3, 2026
e047447
Added changePassword method to the authstore
Darkdusk234 Mar 3, 2026
ff2dcca
Updated submitPasswordChange to now call on the new changePassword me…
Darkdusk234 Mar 3, 2026
edd0be0
Made it so change password button only shows if the user logged in ha…
Darkdusk234 Mar 3, 2026
642c0b8
Removed username from inputted data as it is not needed
Darkdusk234 Mar 4, 2026
6976299
Removed lingering code that referenced username
Darkdusk234 Mar 5, 2026
151f63f
Moved the change password modal out to it's own component
Darkdusk234 Mar 5, 2026
dd3b6c1
Merge branch 'dev' into Feature/Password-Changing-20260302
Darkdusk234 Mar 5, 2026
f3683f1
Changed to daisyUI
Darkdusk234 Mar 6, 2026
5180a82
Merge branch 'dev' into Feature/Password-Changing-20260302
Darkdusk234 Mar 9, 2026
be15a23
Added the change password function to the hamburger menu that is used…
Darkdusk234 Mar 9, 2026
c797406
Merge pull request #1 from immersed-web/dev
Darkdusk234 Apr 2, 2026
884322f
Merge branch 'dev' into Feature/Password-Changing-20260302
Darkdusk234 Apr 2, 2026
5f94bf8
Made it so it is the new password that you need to write twice to con…
Darkdusk234 Apr 10, 2026
3d1be3a
Moved the newpassword confirmation in to the ref
Darkdusk234 Apr 10, 2026
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
116 changes: 116 additions & 0 deletions app/src/components/ChangePasswordModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<dialog
class="modal"
:class="{ 'modal-open': modelValue }"
@click.self="onCancel"
>

<div class="modal-box">
<button @click="onCancel" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
<h2 class="text-lg font-semibold mb-4">Byt lösenord</h2>

<form @submit.prevent="onSubmit" class="space-y-3">
<div class="form-control">
<label class="label">
<span class="label-text">Nuvarande lösenord</span>
</label>
<input
v-model="passwordForm.currentPassword"
type="password"
class="input input-bordered w-full"
:class="{ 'input-error': !passwordsMatch }"
/>
</div>

<div class="form-control">
<label class="label">
<span class="label-text">Nytt lösenord</span>
</label>
<input
v-model="passwordForm.newPassword"
type="password"
class="input input-bordered w-full"
/>
</div>

<div class="form-control">
<label class="label">
<span class="label-text">Bekräfta nytt lösenord</span>
</label>
<input
v-model="passwordForm.newPasswordConfirm"
type="password"
class="input input-bordered w-full"
:class="{ 'input-error': !passwordsMatch }"
/>
<p v-if="!passwordsMatch" class="mt-1 text-sm text-error">
Lösenorden stämmer inte överens.
</p>
</div>

<div class="modal-action">
<button
type="button"
class="btn btn-ghost"
@click="onCancel"
>
Avbryt
</button>
<button type="submit" class="btn btn-primary">
Spara
</button>
</div>
</form>
</div>
</dialog>
</template>

<script setup lang="ts">
import { ref, watch, computed } from 'vue';

const props = defineProps<{
modelValue: boolean
}>();

const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'submit', payload: { currentPassword: string; newPassword: string }): void;
}>();

const emptyForm = () => ({
currentPassword: '',
newPassword: '',
newPasswordConfirm: '',
});

const passwordForm = ref(emptyForm());

const passwordsMatch = computed(() =>
passwordForm.value.newPasswordConfirm === '' ||
passwordForm.value.newPassword === passwordForm.value.newPasswordConfirm,
);

watch(
() => props.modelValue,
(open) => {
if (open) {
passwordForm.value = emptyForm();
passwordForm.value.newPasswordConfirm = '';
}
},
);

function close() {
emit('update:modelValue', false);
}

function onCancel() {
close();
}

function onSubmit() {
if (!passwordsMatch.value) return;
emit('submit', { ...passwordForm.value });
close();
}
</script>
34 changes: 34 additions & 0 deletions app/src/components/layout/DefaultHeader.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<template>
<!-- Modal used for changing passwords -->
<ChangePasswordModal
v-model="showPasswordModal"
@submit="handlePasswordSubmit"
/>

<div data-theme="dark" :style="`background-image: url(${unsplashBackground});`"
class="navbar justify-between gap-14 pl-6">
<div class="">
Expand Down Expand Up @@ -34,6 +40,15 @@
{{ translateUserRole(authStore.role) }}
</span> -->
</div>
<button
v-if="authStore.role === 'user'"
class="btn-ghost shrink-0 btn-error btn btn-circle"
@click="showPasswordModal = true"
>
<span class="material-icons">
password
</span>
</button>
<button @click="logout" class="btn-ghost shrink-0 text-error btn-error btn btn-circle">
<span class="material-icons">
logout
Expand All @@ -50,6 +65,9 @@
{{ route.label }}
</RouterLink>
</li>
<li>
<a @click="changePasswordHamburgerMenu">Ändra lösenord</a>
</li>
<li class="text-error">
<a @click="logout">{{ isAtLeastUser ? 'Logga ut' : 'Avsluta' }}</a>
</li>
Expand All @@ -64,9 +82,25 @@ import { useAuthStore } from '@/stores/authStore';
import { hasAtLeastSecurityRole, translateUserRole } from 'schemas';
import { computed, ref } from 'vue';
import unsplashBackground from '@/assets/milad-fakurian-DX7pT_guAyE-unsplash.jpg';
import ChangePasswordModal from '@/components/ChangePasswordModal.vue';

const router = useRouter();
const authStore = useAuthStore();
const showPasswordModal = ref(false);

async function handlePasswordSubmit(payload: {
currentPassword: string;
newPassword: string;
}) {
await authStore.changePassword(payload);
console.log('Password changed');
showPasswordModal.value = false;
}

const changePasswordHamburgerMenu = async () => {
showPasswordModal.value = true;
closeMenu();
}

const isAtLeastUser = computed(() => {
return authStore.role ? hasAtLeastSecurityRole(authStore.role, 'user') : false;
Expand Down
21 changes: 21 additions & 0 deletions app/src/modules/authClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ if (localMode) {
console.log('authUrl: ', completeAuthUrl);
const authEndpoint = axios.create({ baseURL: completeAuthUrl, withCredentials: true });

interface ChangePasswordRequest {
currentPassword: string;
newPassword: string;
}

export function createUser(username: string, password: string, role: UserRole) {
return handleResponse(() => authEndpoint.post('/user/create', {
role: role.toString(),
Expand Down Expand Up @@ -189,6 +194,22 @@ export const loginWithAutoToken = async (username: string, password: string) =>
return latestJwtToken;
};

export const changePassword = async (data: ChangePasswordRequest) => {
try {
const currentPassword = data.currentPassword;
const newPassword = data.newPassword;

await authEndpoint.post('/user/change-Password', {
currentPassword,
newPassword,
})

return Promise.resolve();
} catch (e) {
return Promise.reject(Error(e.response.data));
}
};

// export const deleteUser = (uuid: string) => authEndpoint.post('delete-user', { uuid });
// export const createUser = (payload: { username: string, password: string, gathering?: string, role: NonGuestUserRole }) => handleResponse<Omit<UserWithIncludes, 'password'>>(() => authEndpoint.post('create', payload));
// export const updateUser = (payload: { uuid: string, username?: string, password?: string, gathering?: string, role?: NonGuestUserRole }) => handleResponse<Omit<UserWithIncludes, 'password'>>(() => authEndpoint.post('update', payload));
Expand Down
12 changes: 11 additions & 1 deletion app/src/stores/authStore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { defineStore } from 'pinia';
import { logout as authLogout, login as authLogin, userAutoToken, guestAutoToken } from '@/modules/authClient';
import { logout as authLogout, login as authLogin, userAutoToken, guestAutoToken, changePassword as authChangePassword } from '@/modules/authClient';
import { hasAtLeastSecurityRole, type JwtPayload } from 'schemas';
import jwtDecode from 'jwt-decode';
import { computed, ref } from 'vue';

interface ChangePasswordRequest {
currentPassword: string;
newPassword: string;
}

const browserHasCookie = () => {
const cookieValue = document.cookie
.split('; ')
Expand Down Expand Up @@ -84,6 +89,10 @@ export const useAuthStore = defineStore('auth', () => {
});
}

async function changePassword(data: ChangePasswordRequest) {
authChangePassword(data);
}

return {
isAuthenticated,
isNotGuest: isLoggedIn,
Expand All @@ -100,6 +109,7 @@ export const useAuthStore = defineStore('auth', () => {
restoreFromSession,
logout,
login,
changePassword,
routePrefix,
};
}, {
Expand Down
122 changes: 122 additions & 0 deletions backend/auth/src/userRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,98 @@ const updateUser: RequestHandler = async (req: UpdateUserRequest, res) => {
}
};

interface ChangePasswordRequest extends ExpressReq {
body: {
currentPassword: string;
newPassword: string;
}
}

const updateUserPassword: RequestHandler = async (req: ChangePasswordRequest, res) => {
const userData = req.session.user;
const data = req.body;
try {
if (!userData) {
throw new Error('no client userdata. unauthorized!');
}

if (userData.role === "guest" || userData.role === "god") {
throw new Error('you have no role! Thus you are not authorized!');
}

if (!data) {
throw new Error('no input data sent!');
}
} catch (e) {
const msg = extractMessageFromCatch(e, 'You give bad data!!!!');
res.status(400).send(msg);
return;
}

const currentUserDatabase = await db.query.users.findFirst({
columns: {
userId: true,
role: true,
password: true,
},
where: (users, { eq }) => eq(users.userId, userData.userId)
})

try {
if (!currentUserDatabase) {
throw new Error('User not found in databse.')
}

if (userData.role !== currentUserDatabase.role) {
throw new Error('Current user data does not match database.')
}
} catch (e) {
const msg = extractMessageFromCatch(e, 'Not authorized!');
res.status(401).send(msg);
return;
}

try {

if (!data.currentPassword) {
throw new Error('Inputted data does not match data in database!')
}

const correct = await bcrypt.compare(data.currentPassword, currentUserDatabase.password);
if (!correct) {
throw new Error('Inputted data does not match data in database!')
}
} catch (e) {
const msg = extractMessageFromCatch(e, 'Not authorized!');
res.status(401).send(msg);
return;
}

let hashedPassword = undefined;
if (data.newPassword) {
hashedPassword = await bcrypt.hash(data.newPassword, 10);
}

const userUpdate = {
password: hashedPassword,
};

try {
const [result] = await db.update(schema.users)
.set(userUpdate)
.where(eq(schema.users.userId, currentUserDatabase.userId))
.returning();
const resultWithoutPassword = exclude(result, 'password');
res.status(201).send(resultWithoutPassword);
return;
} catch (e) {
const errorMsg = extractMessageFromCatch(e, 'failed to update user!');
console.error(errorMsg);
res.status(501).send(errorMsg);
return;
}
}

interface DeleteUserRequest extends ExpressReq {
body: {
userId: UserId
Expand Down Expand Up @@ -283,6 +375,34 @@ const getUsers: RequestHandler = async (req, res) => {
res.send(dbResponse);
}

// const getCurrentUser: RequestHandler = async (req, res) => {
// const userData = req.session.user;
// try {
// if (!userData) {
// throw new Error('no client userdata. unauthorized!');
// }
// if (!userData.role) {
// throw new Error('you have no role! Thus you are not authorized!');
// }
// } catch (e) {
// const msg = extractMessageFromCatch(e, 'You give bad data!!!!');
// res.status(400).send(msg);
// return;
// }

// const dbResponse = await db.query.users.findFirst({
// columns: {
// userId: true,
// username: true,
// role: true,
// password: true,
// },
// where: (users, { eq }) => eq(users.userId, userData.userId)
// })

// res.send(dbResponse);
// }

const getAdmins: RequestHandler = async (req, res) => {
const userData = req.session.user;

Expand Down Expand Up @@ -482,6 +602,8 @@ export default function createUserRouter(): Router {
userRouter.post('/create-sender', isLoggedIn, createSenderForVenue);

userRouter.get('/me', isLoggedIn, getSelf);
userRouter.post('/change-Password', isLoggedIn, updateUserPassword)
// userRouter.get('/get-current-user', isLoggedIn, getCurrentUser)

userRouter.get('/jwt', isLoggedIn, getJwt);

Expand Down