mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
Added mechanism of required password change of new user's first login (#272)
* Deprecate login scenarios that support pre-web era * refactor and simplify setup * Added user info to change password form * change isFistLogin column to shouldChangePassword * Implemented change user password * Implement the change password page for mobile * Change label * Added changes log and up minor version * Fixed typo in the release note * Up server version
This commit is contained in:
75
web/src/routes/auth/change-password/index.svelte
Normal file
75
web/src/routes/auth/change-password/index.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<script context="module" lang="ts">
|
||||
export const prerender = false;
|
||||
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import type { ImmichUser } from '$lib/models/immich-user';
|
||||
|
||||
export const load: Load = async ({ session }) => {
|
||||
if (!session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(serverEndpoint + '/user/me', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + session.user.accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo: ImmichUser = await res.json();
|
||||
|
||||
if (userInfo.shouldChangePassword) {
|
||||
return {
|
||||
status: 200,
|
||||
props: {
|
||||
user: userInfo,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/photos',
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('ERROR Getting user info', e);
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/photos',
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { session } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ChangePasswordForm from '../../../lib/components/forms/change-password-form.svelte';
|
||||
import { serverEndpoint } from '../../../lib/constants';
|
||||
|
||||
export let user: ImmichUser;
|
||||
|
||||
const onSuccessHandler = async () => {
|
||||
const res = await fetch('/auth/logout', { method: 'POST' });
|
||||
|
||||
if (res.status == 200 && res.statusText == 'OK') {
|
||||
goto('/auth/login');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Immich - Change Password</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<ChangePasswordForm {user} on:success={onSuccessHandler} />
|
||||
</div>
|
||||
</section>
|
||||
39
web/src/routes/auth/change-password/index.ts
Normal file
39
web/src/routes/auth/change-password/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
|
||||
export const post: RequestHandler = async ({ request, locals }) => {
|
||||
const form = await request.formData();
|
||||
|
||||
const password = form.get('password');
|
||||
|
||||
const payload = {
|
||||
id: locals.user?.id,
|
||||
password,
|
||||
shouldChangePassword: false,
|
||||
};
|
||||
|
||||
const res = await fetch(`${serverEndpoint}/user`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${locals.user?.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: 'Succesfully change password',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
error: await res.json(),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -3,25 +3,10 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import LoginForm from '$lib/components/forms/login-form.svelte';
|
||||
import UpdateForm from '../../../lib/components/forms/update-form.svelte';
|
||||
import SelectAdminForm from '../../../lib/components/forms/select-admin-form.svelte';
|
||||
|
||||
let shouldShowUpdateForm = false;
|
||||
let shouldShowSelectAdminForm = false;
|
||||
|
||||
const onLoginSuccess = async () => {
|
||||
goto('/photos');
|
||||
};
|
||||
|
||||
const onNeedUpdate = () => {
|
||||
shouldShowUpdateForm = true;
|
||||
shouldShowSelectAdminForm = false;
|
||||
};
|
||||
|
||||
const onNeedSelectAdmin = () => {
|
||||
shouldShowUpdateForm = false;
|
||||
shouldShowSelectAdminForm = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -29,21 +14,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
{#if !shouldShowUpdateForm && !shouldShowSelectAdminForm}
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<LoginForm on:success={onLoginSuccess} on:need-update={onNeedUpdate} on:need-select-admin={onNeedSelectAdmin} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowUpdateForm}
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<UpdateForm on:success={onLoginSuccess} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowSelectAdminForm}
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<SelectAdminForm on:success={onLoginSuccess} />
|
||||
</div>
|
||||
{/if}
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<LoginForm on:success={onLoginSuccess} on:first-login={() => goto('/auth/change-password')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,229 +1,81 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
import * as cookie from 'cookie'
|
||||
import * as cookie from 'cookie';
|
||||
import { getRequest, putRequest } from '$lib/api';
|
||||
|
||||
type LoggedInUser = {
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
type AuthUser = {
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isAdmin: boolean;
|
||||
shouldChangePassword: boolean;
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
const form = await request.formData();
|
||||
|
||||
const email = form.get('email')
|
||||
const password = form.get('password')
|
||||
const email = form.get('email');
|
||||
const password = form.get('password');
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
password,
|
||||
}
|
||||
const payload = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
|
||||
const res = await fetch(`${serverEndpoint}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
const res = await fetch(`${serverEndpoint}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (res.status === 201) {
|
||||
// Login success
|
||||
const loggedInUser = await res.json() as LoggedInUser;
|
||||
if (res.status === 201) {
|
||||
// Login success
|
||||
const authUser = (await res.json()) as AuthUser;
|
||||
|
||||
/**
|
||||
* Support legacy users with two scenario
|
||||
*
|
||||
* Scenario 1 - If one user exists on the server - make the user admin and ask for name.
|
||||
* Scenario 2 - After assigned as admin, scenario 1 user not complete update form with names
|
||||
* Scenario 3 - If two users exists on the server and no admin - ask to choose which one will be made admin
|
||||
*/
|
||||
|
||||
|
||||
// check how many user on the server
|
||||
const { userCount } = await getRequest('user/count', '');
|
||||
const { userCount: adminUserCount } = await getRequest('user/count?isAdmin=true', '')
|
||||
/**
|
||||
* Scenario 1 handler
|
||||
*/
|
||||
if (userCount == 1 && !loggedInUser.isAdmin) {
|
||||
|
||||
const updatedUser = await putRequest('user', {
|
||||
id: loggedInUser.userId,
|
||||
isAdmin: true
|
||||
}, loggedInUser.accessToken)
|
||||
|
||||
|
||||
/**
|
||||
* Scenario 2 handler for current admin user
|
||||
*/
|
||||
let bodyResponse = { success: true, needUpdate: false }
|
||||
|
||||
if (loggedInUser.firstName == "" || loggedInUser.lastName == "") {
|
||||
bodyResponse = { success: false, needUpdate: true }
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
...bodyResponse,
|
||||
user: {
|
||||
id: updatedUser.userId,
|
||||
accessToken: loggedInUser.accessToken,
|
||||
firstName: updatedUser.firstName,
|
||||
lastName: updatedUser.lastName,
|
||||
isAdmin: updatedUser.isAdmin,
|
||||
email: updatedUser.email,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('session', JSON.stringify(
|
||||
{
|
||||
id: updatedUser.userId,
|
||||
accessToken: loggedInUser.accessToken,
|
||||
firstName: updatedUser.firstName,
|
||||
lastName: updatedUser.lastName,
|
||||
isAdmin: updatedUser.isAdmin,
|
||||
email: updatedUser.email,
|
||||
}), {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Scenario 3 handler
|
||||
*/
|
||||
if (userCount >= 2 && adminUserCount == 0) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
needSelectAdmin: true,
|
||||
user: {
|
||||
id: loggedInUser.userId,
|
||||
accessToken: loggedInUser.accessToken,
|
||||
firstName: loggedInUser.firstName,
|
||||
lastName: loggedInUser.lastName,
|
||||
isAdmin: loggedInUser.isAdmin,
|
||||
email: loggedInUser.userEmail
|
||||
},
|
||||
success: 'success'
|
||||
},
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('session', JSON.stringify(
|
||||
{
|
||||
id: loggedInUser.userId,
|
||||
accessToken: loggedInUser.accessToken,
|
||||
firstName: loggedInUser.firstName,
|
||||
lastName: loggedInUser.lastName,
|
||||
isAdmin: loggedInUser.isAdmin,
|
||||
email: loggedInUser.userEmail
|
||||
}), {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario 2 handler
|
||||
*/
|
||||
if (loggedInUser.firstName == "" || loggedInUser.lastName == "") {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
needUpdate: true,
|
||||
user: {
|
||||
id: loggedInUser.userId,
|
||||
accessToken: loggedInUser.accessToken,
|
||||
firstName: loggedInUser.firstName,
|
||||
lastName: loggedInUser.lastName,
|
||||
isAdmin: loggedInUser.isAdmin,
|
||||
email: loggedInUser.userEmail
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('session', JSON.stringify(
|
||||
{
|
||||
id: loggedInUser.userId,
|
||||
accessToken: loggedInUser.accessToken,
|
||||
firstName: loggedInUser.firstName,
|
||||
lastName: loggedInUser.lastName,
|
||||
isAdmin: loggedInUser.isAdmin,
|
||||
email: loggedInUser.userEmail
|
||||
}), {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
user: {
|
||||
id: loggedInUser.userId,
|
||||
accessToken: loggedInUser.accessToken,
|
||||
firstName: loggedInUser.firstName,
|
||||
lastName: loggedInUser.lastName,
|
||||
isAdmin: loggedInUser.isAdmin,
|
||||
email: loggedInUser.userEmail
|
||||
},
|
||||
success: 'success'
|
||||
},
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('session', JSON.stringify(
|
||||
{
|
||||
id: loggedInUser.userId,
|
||||
accessToken: loggedInUser.accessToken,
|
||||
firstName: loggedInUser.firstName,
|
||||
lastName: loggedInUser.lastName,
|
||||
isAdmin: loggedInUser.isAdmin,
|
||||
email: loggedInUser.userEmail,
|
||||
}), {
|
||||
// send cookie for every page
|
||||
path: '/',
|
||||
|
||||
// server side only cookie so you can't use `document.cookie`
|
||||
httpOnly: true,
|
||||
|
||||
// only requests from same site can send cookies
|
||||
// and serves to protect from CSRF
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/CSRF
|
||||
sameSite: 'strict',
|
||||
|
||||
// set cookie to expire after a month
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
error: 'Incorrect email or password'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
user: {
|
||||
id: authUser.userId,
|
||||
accessToken: authUser.accessToken,
|
||||
firstName: authUser.firstName,
|
||||
lastName: authUser.lastName,
|
||||
isAdmin: authUser.isAdmin,
|
||||
email: authUser.userEmail,
|
||||
shouldChangePassword: authUser.shouldChangePassword,
|
||||
},
|
||||
success: 'success',
|
||||
},
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize(
|
||||
'session',
|
||||
JSON.stringify({
|
||||
id: authUser.userId,
|
||||
accessToken: authUser.accessToken,
|
||||
firstName: authUser.firstName,
|
||||
lastName: authUser.lastName,
|
||||
isAdmin: authUser.isAdmin,
|
||||
email: authUser.userEmail,
|
||||
}),
|
||||
{
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
},
|
||||
),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
error: 'Incorrect email or password',
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
<script lang="ts">
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let isAdminUserExist: boolean;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user