mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
Add web interface with admin functionality (#167)
This commit is contained in:
12
web/src/routes/auth/login/api/get-users.ts
Normal file
12
web/src/routes/auth/login/api/get-users.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { getRequest } from '../../../../lib/api';
|
||||
|
||||
|
||||
export const get: RequestHandler = async ({ request, locals }) => {
|
||||
const allUsers = await getRequest('user?isAll=true', locals.user!.accessToken)
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: { allUsers }
|
||||
};
|
||||
}
|
||||
52
web/src/routes/auth/login/api/select-admin.ts
Normal file
52
web/src/routes/auth/login/api/select-admin.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { putRequest } from '$lib/api';
|
||||
import * as cookie from 'cookie';
|
||||
|
||||
export const post: RequestHandler = async ({ request, locals }) => {
|
||||
|
||||
const { id, isAdmin } = await request.json()
|
||||
|
||||
const res = await putRequest('user', {
|
||||
id,
|
||||
isAdmin,
|
||||
}, locals.user!.accessToken);
|
||||
|
||||
|
||||
|
||||
if (res.statusCode) {
|
||||
return {
|
||||
status: res.statusCode,
|
||||
body: JSON.stringify(res)
|
||||
}
|
||||
}
|
||||
|
||||
if (res.id == locals.user!.id) {
|
||||
return {
|
||||
status: 200,
|
||||
body: { userInfo: res },
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('session', JSON.stringify(
|
||||
{
|
||||
id: res.id,
|
||||
accessToken: locals.user!.accessToken,
|
||||
firstName: res.firstName,
|
||||
lastName: res.lastName,
|
||||
isAdmin: res.isAdmin,
|
||||
email: res.email,
|
||||
}), {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: 200,
|
||||
body: { userInfo: { ...locals.user! } },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
49
web/src/routes/auth/login/index.svelte
Normal file
49
web/src/routes/auth/login/index.svelte
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
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>
|
||||
<title>Immich - Login</title>
|
||||
</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}
|
||||
</section>
|
||||
229
web/src/routes/auth/login/index.ts
Normal file
229
web/src/routes/auth/login/index.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
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;
|
||||
}
|
||||
|
||||
export const post: RequestHandler = async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
|
||||
const email = form.get('email')
|
||||
const password = form.get('password')
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
password,
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
63
web/src/routes/auth/login/update.ts
Normal file
63
web/src/routes/auth/login/update.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { putRequest } from '../../../lib/api';
|
||||
import * as cookie from 'cookie'
|
||||
|
||||
|
||||
export const post: RequestHandler = async ({ request, locals }) => {
|
||||
|
||||
const form = await request.formData();
|
||||
|
||||
const firstName = form.get('firstName')
|
||||
const lastName = form.get('lastName')
|
||||
|
||||
if (locals.user) {
|
||||
const updatedUser = await putRequest('user', {
|
||||
id: locals.user.id,
|
||||
firstName,
|
||||
lastName
|
||||
}, locals.user.accessToken)
|
||||
|
||||
|
||||
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
user: {
|
||||
id: updatedUser.id,
|
||||
accessToken: locals.user.accessToken,
|
||||
firstName: updatedUser.firstName,
|
||||
lastName: updatedUser.lastName,
|
||||
isAdmin: updatedUser.isAdmin,
|
||||
email: updatedUser.email,
|
||||
},
|
||||
success: 'Update user success'
|
||||
},
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('session', JSON.stringify(
|
||||
{
|
||||
id: updatedUser.id,
|
||||
accessToken: locals.user.accessToken,
|
||||
firstName: updatedUser.firstName,
|
||||
lastName: updatedUser.lastName,
|
||||
isAdmin: updatedUser.isAdmin,
|
||||
email: updatedUser.email,
|
||||
}), {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
error: 'Cannot get access token from cookies'
|
||||
}
|
||||
}
|
||||
}
|
||||
39
web/src/routes/auth/register/index.svelte
Normal file
39
web/src/routes/auth/register/index.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
|
||||
export const load: Load = async ({ session, fetch }) => {
|
||||
const res = await fetch(`${serverEndpoint}/user/count`);
|
||||
const { userCount } = await res.json();
|
||||
|
||||
if (userCount != 0) {
|
||||
// Admin has been registered, redirect to login
|
||||
|
||||
if (!session.user) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/auth/login',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/dashboard',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Immich - Admin Registration</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<AdminRegistrationForm />
|
||||
</section>
|
||||
43
web/src/routes/auth/register/index.ts
Normal file
43
web/src/routes/auth/register/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { serverEndpoint } from '$lib/constants';
|
||||
|
||||
export const post: RequestHandler = async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
|
||||
const email = form.get('email')
|
||||
const password = form.get('password')
|
||||
const firstName = form.get('firstName')
|
||||
const lastName = form.get('lastName')
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}
|
||||
|
||||
const res = await fetch(`${serverEndpoint}/auth/admin-sign-up`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (res.status === 201) {
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
success: 'Succesfully create admin account'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
error: await res.json()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user