feat(web)!: SPA (#5069)

* feat(web): SPA

* chore: remove unnecessary prune

* feat(web): merge with immich-server

* Correct method name

* fix: bugs, docs, workflows, etc.

* chore: keep dockerignore for dev

* chore: remove license

* fix: expose 2283

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2023-11-17 23:13:36 -05:00
committed by GitHub
parent 5118d261ab
commit adae5dd758
115 changed files with 730 additions and 1446 deletions

View File

@@ -26,7 +26,7 @@ import { BASE_PATH } from './open-api/base';
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
import type { ApiParams } from './types';
export class ImmichApi {
class ImmichApi {
public activityApi: ActivityApi;
public albumApi: AlbumApi;
public libraryApi: LibraryApi;

5
web/src/app.d.ts vendored
View File

@@ -3,11 +3,6 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
interface Locals {
user?: import('@api').UserResponseDto;
api: import('@api').ImmichApi;
}
interface PageData {
meta: {
title: string;

View File

@@ -13,6 +13,7 @@
document.documentElement.classList.remove('dark');
}
</script>
<link rel="stylesheet" href="/custom.css" />
</head>
<body class="bg-immich-bg dark:bg-immich-dark-bg">

40
web/src/hooks.client.ts Normal file
View File

@@ -0,0 +1,40 @@
import type { HandleClientError } from '@sveltejs/kit';
import type { AxiosError, AxiosResponse } from 'axios';
const LOG_PREFIX = '[hooks.client.ts]';
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
const parseError = (error: unknown) => {
const httpError = error as AxiosError;
const request = httpError?.request as Request & { path: string };
const response = httpError?.response as AxiosResponse<{
message: string;
statusCode: number;
error: string;
}>;
let code = response?.data?.statusCode || response?.status || httpError.code || '500';
if (response) {
code += ` - ${response.data?.error || response.statusText}`;
}
if (request && response) {
console.log({
status: response.status,
url: `${request.method} ${request.path}`,
response: response.data || 'No data',
});
}
return {
message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
code,
stack: httpError?.stack,
};
};
export const handleError: HandleClientError = ({ error }) => {
const result = parseError(error);
console.error(`${LOG_PREFIX}:handleError ${result.message}`);
return result;
};

View File

@@ -1,77 +0,0 @@
import { env } from '$env/dynamic/public';
import type { Handle, HandleServerError } from '@sveltejs/kit';
import type { AxiosError, AxiosResponse } from 'axios';
import { ImmichApi } from './api/api';
const LOG_PREFIX = '[hooks.server.ts]';
export const handle = (async ({ event, resolve }) => {
const basePath = env.PUBLIC_IMMICH_SERVER_URL || 'http://immich-server:3001';
const accessToken = event.cookies.get('immich_access_token');
const api = new ImmichApi({ basePath, accessToken });
// API instance that should be used for all server-side requests.
event.locals.api = api;
if (accessToken) {
try {
const { data: user } = await api.userApi.getMyUserInfo();
event.locals.user = user;
} catch (err) {
console.log(`${LOG_PREFIX} Unable to get my user`, parseError(err));
const apiError = err as AxiosError;
// Ignore 401 unauthorized errors and log all others.
if (apiError.response?.status && apiError.response?.status !== 401) {
console.error(`${LOG_PREFIX}:handle`, err);
} else if (!apiError.response?.status) {
console.error(`${LOG_PREFIX}:handle`, apiError?.message);
}
}
}
const res = await resolve(event);
// The link header can grow quite big and has caused issues with our nginx
// proxy returning a 502 Bad Gateway error. Therefore the header gets deleted.
res.headers.delete('Link');
return res;
}) satisfies Handle;
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
const parseError = (error: unknown) => {
const httpError = error as AxiosError;
const request = httpError?.request as Request & { path: string };
const response = httpError?.response as AxiosResponse<{
message: string;
statusCode: number;
error: string;
}>;
let code = response?.data?.statusCode || response?.status || httpError.code || '500';
if (response) {
code += ` - ${response.data?.error || response.statusText}`;
}
if (request && response) {
console.log({
status: response.status,
url: `${request.method} ${request.path}`,
response: response.data || 'No data',
});
}
return {
message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
code,
stack: httpError?.stack,
};
};
export const handleError: HandleServerError = ({ error }) => {
const result = parseError(error);
console.error(`${LOG_PREFIX}:handleError ${result.message}`);
return result;
};

View File

@@ -26,9 +26,6 @@
const logOut = async () => {
const { data } = await api.authenticationApi.logout();
await fetch('/auth/logout', { method: 'POST' });
goto(data.redirectUri || '/auth/login?autoLaunch=0');
};
</script>

34
web/src/lib/utils/auth.ts Normal file
View File

@@ -0,0 +1,34 @@
import { api } from '@api';
import { redirect } from '@sveltejs/kit';
import { AppRoute } from '../constants';
export interface AuthOptions {
admin?: true;
}
export const getAuthUser = async () => {
try {
const { data: user } = await api.userApi.getMyUserInfo();
return user;
} catch {
return null;
}
};
// TODO: re-use already loaded user (once) instead of fetching on each page navigation
export const authenticate = async (options?: AuthOptions) => {
options = options || {};
const user = await getAuthUser();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
if (options.admin && !user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
return user;
};
export const isLoggedIn = async () => getAuthUser().then((user) => !!user);

View File

@@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { api, user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
try {
const { data: albums } = await api.albumApi.getAllAlbums();
return {
user: user,
albums: albums,
meta: {
title: 'Albums',
},
};
} catch (e) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: albums } = await api.albumApi.getAllAlbums();
return {
user,
albums,
meta: {
title: 'Albums',
},
};
}) satisfies PageLoad;

View File

@@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ params, locals: { api, user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
try {
const { data: album } = await api.albumApi.getAlbumInfo({ id: params.albumId, withoutAssets: true });
return {
album,
user,
meta: {
title: album.albumName,
},
};
} catch (e) {
throw redirect(302, AppRoute.ALBUMS);
}
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const user = await authenticate();
const { data: album } = await api.albumApi.getAlbumInfo({ id: params.albumId, withoutAssets: true });
return {
album,
user,
meta: {
title: album.albumName,
},
};
}) satisfies PageLoad;

View File

@@ -1,15 +1,9 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ params, parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const albumId = params['albumId'];
export const load: PageLoad = async ({ params }) => {
const albumId = params.albumId;
if (albumId) {
throw redirect(302, `${AppRoute.ALBUMS}/${albumId}`);

View File

@@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Archive',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,13 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Archive',
},
};
}) satisfies PageLoad;

View File

@@ -1,13 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
export const load: PageLoad = async () => {
throw redirect(302, AppRoute.ARCHIVE);
};

View File

@@ -1,21 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: items } = await locals.api.searchApi.getExploreData();
const { data: response } = await locals.api.personApi.getAllPeople({ withHidden: false });
return {
user,
items,
response,
meta: {
title: 'Explore',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,17 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: items } = await api.searchApi.getExploreData();
const { data: response } = await api.personApi.getAllPeople({ withHidden: false });
return {
user,
items,
response,
meta: {
title: 'Explore',
},
};
}) satisfies PageLoad;

View File

@@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Favorites',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Favorites',
},
};
}) satisfies PageLoad;

View File

@@ -1,15 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { AppRoute } from '$lib/constants';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else {
throw redirect(302, AppRoute.FAVORITES);
}
};

View File

@@ -0,0 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
throw redirect(302, AppRoute.FAVORITES);
};

View File

@@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Map',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Map',
},
};
}) satisfies PageLoad;

View File

@@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Memory',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Memory',
},
};
}) satisfies PageLoad;

View File

@@ -1,15 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { AppRoute } from '$lib/constants';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else {
throw redirect(302, AppRoute.MEMORY);
}
};

View File

@@ -0,0 +1,9 @@
import { AppRoute } from '$lib/constants';
import { authenticate } from '$lib/utils/auth';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
throw redirect(302, AppRoute.MEMORY);
}) satisfies PageLoad;

View File

@@ -1,15 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { AppRoute } from '$lib/constants';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else {
throw redirect(302, AppRoute.MEMORY);
}
};

View File

@@ -0,0 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
throw redirect(302, AppRoute.PHOTOS);
}) satisfies PageLoad;

View File

@@ -1,21 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, parent, locals: { api } }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: partner } = await api.userApi.getUserById({ id: params['userId'] });
return {
user,
partner,
meta: {
title: 'Partner',
},
};
};

View File

@@ -0,0 +1,17 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const user = await authenticate();
const { data: partner } = await api.userApi.getUserById({ id: params.userId });
return {
user,
partner,
meta: {
title: 'Partner',
},
};
}) satisfies PageLoad;

View File

@@ -1,19 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: people } = await locals.api.personApi.getAllPeople({ withHidden: true });
return {
user,
people,
meta: {
title: 'People',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: people } = await api.personApi.getAllPeople({ withHidden: true });
return {
user,
people,
meta: {
title: 'People',
},
};
}) satisfies PageLoad;

View File

@@ -1,22 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, parent, params }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: person } = await locals.api.personApi.getPerson({ id: params.personId });
const { data: statistics } = await locals.api.personApi.getPersonStatistics({ id: params.personId });
return {
user,
person,
statistics,
meta: {
title: person.name || 'Person',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,19 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const user = await authenticate();
const { data: person } = await api.personApi.getPerson({ id: params.personId });
const { data: statistics } = await api.personApi.getPersonStatistics({ id: params.personId });
return {
user,
person,
statistics,
meta: {
title: person.name || 'Person',
},
};
}) satisfies PageLoad;

View File

@@ -1,14 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ params, parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const personId = params['personId'];
throw redirect(302, `${AppRoute.PEOPLE}/${personId}`);
};
export const load = (async ({ params }) => {
throw redirect(302, `${AppRoute.PEOPLE}/${params.personId}`);
}) satisfies PageLoad;

View File

@@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Photos',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Photos',
},
};
}) satisfies PageLoad;

View File

@@ -1,15 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { AppRoute } from '$lib/constants';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else {
throw redirect(302, AppRoute.PHOTOS);
}
};

View File

@@ -0,0 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
throw redirect(302, AppRoute.PHOTOS);
}) satisfies PageLoad;

View File

@@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, parent, url }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined;
const { data: results } = await locals.api.searchApi.search({}, { params: url.searchParams });
return {
user,
term,
results,
meta: {
title: 'Search',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,20 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const url = new URL(location.href);
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined;
const { data: results } = await api.searchApi.search({}, { params: url.searchParams });
return {
user,
term,
results,
meta: {
title: 'Search',
},
};
}) satisfies PageLoad;

View File

@@ -1,13 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
export const load = (async () => {
throw redirect(302, AppRoute.SEARCH);
};
}) satisfies PageLoad;

View File

@@ -1,31 +1,32 @@
import featurePanelUrl from '$lib/assets/feature-panel.png';
import { api as clientApi, ThumbnailFormat } from '@api';
import { getAuthUser } from '$lib/utils/auth';
import { api, ThumbnailFormat } from '@api';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import type { AxiosError } from 'axios';
import type { PageLoad } from './$types';
export const load = (async ({ params, locals: { api }, cookies }) => {
export const load = (async ({ params }) => {
const { key } = params;
const token = cookies.get('immich_shared_link_token');
const user = await getAuthUser();
try {
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key, token });
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key });
const assetCount = sharedLink.assets.length;
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
return {
user,
sharedLink,
meta: {
title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
description: sharedLink.description || `${assetCount} shared photos & videos.`,
imageUrl: assetId
? clientApi.getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key)
: featurePanelUrl,
imageUrl: assetId ? api.getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key) : featurePanelUrl,
},
};
} catch (e) {
// handle unauthorized error
// TODO this doesn't allow for 404 shared links anymore
if ((e as AxiosError).response?.status === 401) {
return {
passwordRequired: true,
@@ -40,4 +41,4 @@ export const load = (async ({ params, locals: { api }, cookies }) => {
message: 'Invalid shared link',
});
}
}) satisfies PageServerLoad;
}) satisfies PageLoad;

View File

@@ -1,19 +0,0 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ params, locals: { api } }) => {
const { key, assetId } = params;
const { data: asset } = await api.assetApi.getAssetById({ id: assetId, key });
if (!asset) {
throw error(404, 'Asset not found');
}
return {
asset,
key,
meta: {
title: 'Public Share',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,15 @@
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const { key, assetId } = params;
const { data: asset } = await api.assetApi.getAssetById({ id: assetId, key });
return {
asset,
key,
meta: {
title: 'Public Share',
},
};
}) satisfies PageLoad;

View File

@@ -1,26 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { api, user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
try {
const { data: sharedAlbums } = await api.albumApi.getAllAlbums({ shared: true });
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-with' });
return {
user,
sharedAlbums,
partners,
meta: {
title: 'Sharing',
},
};
} catch (e) {
console.log(e);
throw redirect(302, AppRoute.AUTH_LOGIN);
}
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,18 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: sharedAlbums } = await api.albumApi.getAllAlbums({ shared: true });
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-with' });
return {
user,
sharedAlbums,
partners,
meta: {
title: 'Sharing',
},
};
}) satisfies PageLoad;

View File

@@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Shared Links',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Shared Links',
},
};
}) satisfies PageLoad;

View File

@@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Trash',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Trash',
},
};
}) satisfies PageLoad;

View File

@@ -1,13 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
export const load = (async () => {
throw redirect(302, AppRoute.TRASH);
};
}) satisfies PageLoad;

View File

@@ -1,22 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ parent, locals }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: keys } = await locals.api.keyApi.getApiKeys();
const { data: devices } = await locals.api.authenticationApi.getAuthDevices();
return {
user,
keys,
devices,
meta: {
title: 'Settings',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,19 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: keys } = await api.keyApi.getApiKeys();
const { data: devices } = await api.authenticationApi.getAuthDevices();
return {
user,
keys,
devices,
meta: {
title: 'Settings',
},
};
}) satisfies PageLoad;

View File

@@ -1,5 +0,0 @@
import type { LayoutServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
return { user };
}) satisfies LayoutServerLoad;

View File

@@ -24,7 +24,10 @@
export let data: LayoutData;
let albumId: string | undefined;
if ($page.route.id?.startsWith('/(user)/share/[key]')) {
const isSharedLinkRoute = (route: string | null) => route?.startsWith('/(user)/share/[key]');
const isAuthRoute = (route?: string) => route?.startsWith('/auth');
if (isSharedLinkRoute($page.route?.id)) {
api.setKey($page.params.key);
}
@@ -32,11 +35,11 @@
const fromRoute = from?.route?.id || '';
const toRoute = to?.route?.id || '';
if (fromRoute.startsWith('/auth') && !toRoute.startsWith('/auth')) {
if (isAuthRoute(fromRoute) && !isAuthRoute(toRoute)) {
openWebsocketConnection();
}
if (!fromRoute.startsWith('/auth') && toRoute.startsWith('/auth')) {
if (!isAuthRoute(fromRoute) && isAuthRoute(toRoute)) {
closeWebsocketConnection();
}
@@ -80,7 +83,6 @@
<svelte:head>
<title>{$page.data.meta?.title || 'Web'} - Immich</title>
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/custom.css" />
<meta name="theme-color" content="currentColor" />
<FaviconHeader />
<AppleHeader />

25
web/src/routes/+layout.ts Normal file
View File

@@ -0,0 +1,25 @@
import { api } from '../api';
import type { LayoutLoad } from './$types';
const getUser = async () => {
try {
const { data: user } = await api.userApi.getMyUserInfo();
return user;
} catch {
return null;
}
};
export const ssr = false;
export const csr = true;
export const load = (async () => {
const user = await getUser();
return {
user,
meta: {
title: 'Immich',
},
};
}) satisfies LayoutLoad;

View File

@@ -1,17 +1,19 @@
export const prerender = false;
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { api } from '../api';
import { isLoggedIn } from '../lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async ({ parent, locals: { api } }) => {
const { user } = await parent();
if (user) {
export const ssr = false;
export const csr = true;
export const load = (async () => {
const authenticated = await isLoggedIn();
if (authenticated) {
throw redirect(302, AppRoute.PHOTOS);
}
const { data } = await api.serverInfoApi.getServerConfig();
if (data.isInitialized) {
// Redirect to login page if there exists an admin account (i.e. server is initialized)
throw redirect(302, AppRoute.AUTH_LOGIN);
@@ -23,4 +25,4 @@ export const load = (async ({ parent, locals: { api } }) => {
description: 'Immich Web Interface',
},
};
}) satisfies PageServerLoad;
}) satisfies PageLoad;

View File

@@ -1,11 +0,0 @@
import { json } from '@sveltejs/kit';
const endpoint = process.env.IMMICH_API_URL_EXTERNAL || '/api';
export const GET = async () => {
return json({
api: {
endpoint,
},
});
};

View File

@@ -1,15 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
throw redirect(302, AppRoute.ADMIN_USER_MANAGEMENT);
};

View File

@@ -0,0 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
throw redirect(302, AppRoute.ADMIN_USER_MANAGEMENT);
}) satisfies PageLoad;

View File

@@ -1,26 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user, api } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
try {
const { data: jobs } = await api.jobApi.getAllJobsStatus();
return {
user,
jobs,
meta: {
title: 'Job Status',
},
};
} catch (err) {
console.error('[jobs] > getAllJobsStatus', err);
throw err;
}
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,17 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate({ admin: true });
const { data: jobs } = await api.jobApi.getAllJobsStatus();
return {
user,
jobs,
meta: {
title: 'Job Status',
},
};
}) satisfies PageLoad;

View File

@@ -1,26 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ parent, locals: { api } }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
const {
data: { orphans, extras },
} = await api.auditApi.getAuditFiles();
return {
user,
orphans,
extras,
meta: {
title: 'Repair',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,19 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate({ admin: true });
const {
data: { orphans, extras },
} = await api.auditApi.getAuditFiles();
return {
user,
orphans,
extras,
meta: {
title: 'Repair',
},
};
}) satisfies PageLoad;

View File

@@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ parent, locals: { api } }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
const { data: stats } = await api.serverInfoApi.getServerStatistics();
return {
user,
stats,
meta: {
title: 'Server Stats',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate({ admin: true });
const { data: stats } = await api.serverInfoApi.getServerStatistics();
return {
user,
stats,
meta: {
title: 'Server Stats',
},
};
}) satisfies PageLoad;

View File

@@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent, locals: { api } }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
const { data: configs } = await api.systemConfigApi.getConfig();
return {
user,
configs,
meta: {
title: 'System Settings',
},
};
};

View File

@@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate({ admin: true });
const { data: configs } = await api.systemConfigApi.getConfig();
return {
user,
configs,
meta: {
title: 'System Settings',
},
};
}) satisfies PageLoad;

View File

@@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ parent, locals: { api } }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
const { data: allUsers } = await api.userApi.getAllUsers({ isAll: false });
return {
user,
allUsers,
meta: {
title: 'User Management',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate({ admin: true });
const { data: allUsers } = await api.userApi.getAllUsers({ isAll: false });
return {
user,
allUsers,
meta: {
title: 'User Management',
},
};
}) satisfies PageLoad;

View File

@@ -1,18 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.shouldChangePassword) {
throw redirect(302, AppRoute.PHOTOS);
}
return {
user,
meta: {
title: 'Change Password',
},
};
}) satisfies PageServerLoad;

View File

@@ -0,0 +1,18 @@
import { AppRoute } from '$lib/constants';
import { authenticate } from '$lib/utils/auth';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
if (!user.shouldChangePassword) {
throw redirect(302, AppRoute.PHOTOS);
}
return {
user,
meta: {
title: 'Change Password',
},
};
}) satisfies PageLoad;

View File

@@ -1,8 +1,9 @@
import { AppRoute } from '$lib/constants';
import { api } from '@api';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import type { PageLoad } from './$types';
export const load = (async ({ locals: { api } }) => {
export const load = (async () => {
const { data } = await api.serverInfoApi.getServerConfig();
if (!data.isInitialized) {
// Admin not registered
@@ -14,4 +15,4 @@ export const load = (async ({ locals: { api } }) => {
title: 'Login',
},
};
}) satisfies PageServerLoad;
}) satisfies PageLoad;

View File

@@ -1,12 +0,0 @@
import { api } from '@api';
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
export const POST = (async ({ cookies }) => {
api.removeAccessToken();
cookies.delete('immich_auth_type', { path: '/' });
cookies.delete('immich_access_token', { path: '/' });
return json({ ok: true });
}) satisfies RequestHandler;

View File

@@ -1,8 +1,9 @@
import { AppRoute } from '$lib/constants';
import { api } from '@api';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import type { PageLoad } from './$types';
export const load = (async ({ locals: { api } }) => {
export const load = (async () => {
const { data } = await api.serverInfoApi.getServerConfig();
if (data.isInitialized) {
// Admin has been registered, redirect to login
@@ -14,4 +15,4 @@ export const load = (async ({ locals: { api } }) => {
title: 'Admin Registration',
},
};
}) satisfies PageServerLoad;
}) satisfies PageLoad;

View File

@@ -1,11 +0,0 @@
import { RequestHandler, text } from '@sveltejs/kit';
export const GET = (async ({ locals: { api } }) => {
const {
data: { customCss },
} = await api.serverInfoApi.getTheme();
return text(customCss, {
headers: {
'Content-Type': 'text/css',
},
});
}) satisfies RequestHandler;