1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-17 01:06:46 +01:00

Use cookies for client requests (#377)

* Use cookie for frontend request

* Remove api helper to use SDK

* Added error handling to status box

* Remove additional places that check for session.user

* Refactor sending password

* prettier clean up

* remove deadcode

* Move all authentication requests to the client

* refactor upload panel to only fetch assets after the upload panel disappear

* Added keydown to remove focus on title change on album viewer
This commit is contained in:
Alex 2022-07-26 12:28:07 -05:00 committed by GitHub
parent 2ebb755f00
commit 83cbf51704
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 4954 additions and 4540 deletions

View file

@ -70,6 +70,8 @@ services:
- ../web:/usr/src/app - ../web:/usr/src/app
- /usr/src/app/node_modules - /usr/src/app/node_modules
restart: always restart: always
depends_on:
- immich-server
redis: redis:
container_name: immich_redis container_name: immich_redis

View file

@ -16,10 +16,17 @@ export class AdminRolesGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
let accessToken = '';
if (request.headers['authorization']) { if (request.headers['authorization']) {
const bearerToken = request.headers['authorization'].split(' ')[1]; accessToken = request.headers['authorization'].split(' ')[1];
const { userId } = await this.jwtService.validateToken(bearerToken); } else if (request.cookies['immich_access_token']) {
accessToken = request.cookies['immich_access_token'];
} else {
return false;
}
const { userId } = await this.jwtService.validateToken(accessToken);
if (!userId) { if (!userId) {
return false; return false;
@ -32,7 +39,4 @@ export class AdminRolesGuard implements CanActivate {
return user.isAdmin; return user.isAdmin;
} }
return false;
}
} }

View file

@ -1,6 +1,6 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {}
},
} }
};

View file

@ -30,6 +30,10 @@ class ImmichApi {
public setAccessToken(accessToken: string) { public setAccessToken(accessToken: string) {
this.config.accessToken = accessToken; this.config.accessToken = accessToken;
} }
public removeAccessToken() {
this.config.accessToken = undefined;
}
} }
export const api = new ImmichApi(); export const api = new ImmichApi();

File diff suppressed because it is too large Load diff

View file

@ -12,23 +12,22 @@
* Do not edit the class manually. * Do not edit the class manually.
*/ */
import { Configuration } from './configuration';
import { Configuration } from "./configuration";
// Some imports not used depending on template conditions // Some imports not used depending on template conditions
// @ts-ignore // @ts-ignore
import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
export const BASE_PATH = "/api".replace(/\/+$/, ""); export const BASE_PATH = '/api'.replace(/\/+$/, '');
/** /**
* *
* @export * @export
*/ */
export const COLLECTION_FORMATS = { export const COLLECTION_FORMATS = {
csv: ",", csv: ',',
ssv: " ", ssv: ' ',
tsv: "\t", tsv: '\t',
pipes: "|", pipes: '|'
}; };
/** /**
@ -49,13 +48,17 @@ export interface RequestArgs {
export class BaseAPI { export class BaseAPI {
protected configuration: Configuration | undefined; protected configuration: Configuration | undefined;
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { constructor(
configuration?: Configuration,
protected basePath: string = BASE_PATH,
protected axios: AxiosInstance = globalAxios
) {
if (configuration) { if (configuration) {
this.configuration = configuration; this.configuration = configuration;
this.basePath = configuration.basePath || this.basePath; this.basePath = configuration.basePath || this.basePath;
} }
} }
}; }
/** /**
* *
@ -64,7 +67,7 @@ export class BaseAPI {
* @extends {Error} * @extends {Error}
*/ */
export class RequiredError extends Error { export class RequiredError extends Error {
name: "RequiredError" = "RequiredError"; name: 'RequiredError' = 'RequiredError';
constructor(public field: string, msg?: string) { constructor(public field: string, msg?: string) {
super(msg); super(msg);
} }

View file

@ -12,40 +12,51 @@
* Do not edit the class manually. * Do not edit the class manually.
*/ */
import { Configuration } from './configuration';
import { Configuration } from "./configuration"; import { RequiredError, RequestArgs } from './base';
import { RequiredError, RequestArgs } from "./base";
import { AxiosInstance, AxiosResponse } from 'axios'; import { AxiosInstance, AxiosResponse } from 'axios';
/** /**
* *
* @export * @export
*/ */
export const DUMMY_BASE_URL = 'https://example.com' export const DUMMY_BASE_URL = 'https://example.com';
/** /**
* *
* @throws {RequiredError} * @throws {RequiredError}
* @export * @export
*/ */
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { export const assertParamExists = function (
functionName: string,
paramName: string,
paramValue: unknown
) {
if (paramValue === null || paramValue === undefined) { if (paramValue === null || paramValue === undefined) {
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); throw new RequiredError(
} paramName,
`Required parameter ${paramName} was null or undefined when calling ${functionName}.`
);
} }
};
/** /**
* *
* @export * @export
*/ */
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { export const setApiKeyToObject = async function (
object: any,
keyParamName: string,
configuration?: Configuration
) {
if (configuration && configuration.apiKey) { if (configuration && configuration.apiKey) {
const localVarApiKeyValue = typeof configuration.apiKey === 'function' const localVarApiKeyValue =
typeof configuration.apiKey === 'function'
? await configuration.apiKey(keyParamName) ? await configuration.apiKey(keyParamName)
: await configuration.apiKey; : await configuration.apiKey;
object[keyParamName] = localVarApiKeyValue; object[keyParamName] = localVarApiKeyValue;
} }
} };
/** /**
* *
@ -53,9 +64,9 @@ export const setApiKeyToObject = async function (object: any, keyParamName: stri
*/ */
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
if (configuration && (configuration.username || configuration.password)) { if (configuration && (configuration.username || configuration.password)) {
object["auth"] = { username: configuration.username, password: configuration.password }; object['auth'] = { username: configuration.username, password: configuration.password };
}
} }
};
/** /**
* *
@ -63,25 +74,32 @@ export const setBasicAuthToObject = function (object: any, configuration?: Confi
*/ */
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
if (configuration && configuration.accessToken) { if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function' const accessToken =
typeof configuration.accessToken === 'function'
? await configuration.accessToken() ? await configuration.accessToken()
: await configuration.accessToken; : await configuration.accessToken;
object["Authorization"] = "Bearer " + accessToken; object['Authorization'] = 'Bearer ' + accessToken;
}
} }
};
/** /**
* *
* @export * @export
*/ */
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { export const setOAuthToObject = async function (
object: any,
name: string,
scopes: string[],
configuration?: Configuration
) {
if (configuration && configuration.accessToken) { if (configuration && configuration.accessToken) {
const localVarAccessTokenValue = typeof configuration.accessToken === 'function' const localVarAccessTokenValue =
typeof configuration.accessToken === 'function'
? await configuration.accessToken(name, scopes) ? await configuration.accessToken(name, scopes)
: await configuration.accessToken; : await configuration.accessToken;
object["Authorization"] = "Bearer " + localVarAccessTokenValue; object['Authorization'] = 'Bearer ' + localVarAccessTokenValue;
}
} }
};
/** /**
* *
@ -102,37 +120,51 @@ export const setSearchParams = function (url: URL, ...objects: any[]) {
} }
} }
url.search = searchParams.toString(); url.search = searchParams.toString();
} };
/** /**
* *
* @export * @export
*/ */
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { export const serializeDataIfNeeded = function (
value: any,
requestOptions: any,
configuration?: Configuration
) {
const nonString = typeof value !== 'string'; const nonString = typeof value !== 'string';
const needsSerialization = nonString && configuration && configuration.isJsonMime const needsSerialization =
nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers['Content-Type']) ? configuration.isJsonMime(requestOptions.headers['Content-Type'])
: nonString; : nonString;
return needsSerialization return needsSerialization ? JSON.stringify(value !== undefined ? value : {}) : value || '';
? JSON.stringify(value !== undefined ? value : {}) };
: (value || "");
}
/** /**
* *
* @export * @export
*/ */
export const toPathString = function (url: URL) { export const toPathString = function (url: URL) {
return url.pathname + url.search + url.hash return url.pathname + url.search + url.hash;
} };
/** /**
* *
* @export * @export
*/ */
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { export const createRequestFunction = function (
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { axiosArgs: RequestArgs,
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url}; globalAxios: AxiosInstance,
BASE_PATH: string,
configuration?: Configuration
) {
return <T = unknown, R = AxiosResponse<T>>(
axios: AxiosInstance = globalAxios,
basePath: string = BASE_PATH
) => {
const axiosRequestArgs = {
...axiosArgs.options,
url: (configuration?.basePath || basePath) + axiosArgs.url
};
return axios.request<T, R>(axiosRequestArgs); return axios.request<T, R>(axiosRequestArgs);
}; };
} };

View file

@ -12,12 +12,19 @@
* Do not edit the class manually. * Do not edit the class manually.
*/ */
export interface ConfigurationParameters { export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>); apiKey?:
| string
| Promise<string>
| ((name: string) => string)
| ((name: string) => Promise<string>);
username?: string; username?: string;
password?: string; password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>); accessToken?:
| string
| Promise<string>
| ((name?: string, scopes?: string[]) => string)
| ((name?: string, scopes?: string[]) => Promise<string>);
basePath?: string; basePath?: string;
baseOptions?: any; baseOptions?: any;
formDataCtor?: new () => any; formDataCtor?: new () => any;
@ -29,7 +36,11 @@ export class Configuration {
* @param name security name * @param name security name
* @memberof Configuration * @memberof Configuration
*/ */
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>); apiKey?:
| string
| Promise<string>
| ((name: string) => string)
| ((name: string) => Promise<string>);
/** /**
* parameter for basic security * parameter for basic security
* *
@ -50,7 +61,11 @@ export class Configuration {
* @param scopes oauth2 scope * @param scopes oauth2 scope
* @memberof Configuration * @memberof Configuration
*/ */
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>); accessToken?:
| string
| Promise<string>
| ((name?: string, scopes?: string[]) => string)
| ((name?: string, scopes?: string[]) => Promise<string>);
/** /**
* override base path * override base path
* *
@ -95,7 +110,12 @@ export class Configuration {
* @return True if the given MIME is JSON, false otherwise. * @return True if the given MIME is JSON, false otherwise.
*/ */
public isJsonMime(mime: string): boolean { public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); const jsonMime: RegExp = new RegExp(
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); '^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$',
'i'
);
return (
mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json')
);
} }
} }

View file

@ -12,7 +12,5 @@
* Do not edit the class manually. * Do not edit the class manually.
*/ */
export * from './api';
export * from "./api"; export * from './configuration';
export * from "./configuration";

19
web/src/app.d.ts vendored
View file

@ -4,29 +4,14 @@
// for information about these interfaces // for information about these interfaces
declare namespace App { declare namespace App {
interface Locals { interface Locals {
user?: { user?: import('@api').UserResponseDto;
id: string,
email: string,
accessToken: string,
firstName: string,
lastName: string,
isAdmin: boolean,
}
} }
// interface Platform {} // interface Platform {}
interface Session { interface Session {
user?: { user?: import('@api').UserResponseDto;
id: string,
email: string,
accessToken: string,
firstName: string,
lastName: string
isAdmin: boolean,
}
} }
// interface Stuff {} // interface Stuff {}
} }

View file

@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
@ -11,5 +10,4 @@
<body> <body>
<div>%sveltekit.body%</div> <div>%sveltekit.body%</div>
</body> </body>
</html> </html>

View file

@ -1,36 +1,23 @@
import type { GetSession, Handle } from '@sveltejs/kit'; import type { ExternalFetch, GetSession, Handle } from '@sveltejs/kit';
import * as cookie from 'cookie'; import * as cookie from 'cookie';
import { api } from '@api'; import { api } from '@api';
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
const cookies = cookie.parse(event.request.headers.get('cookie') || ''); const cookies = cookie.parse(event.request.headers.get('cookie') || '');
if (!cookies.session) { if (!cookies['immich_is_authenticated']) {
return await resolve(event); return await resolve(event);
} }
const accessToken = cookies['immich_access_token'];
try { try {
const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session);
api.setAccessToken(accessToken); api.setAccessToken(accessToken);
const { status } = await api.authenticationApi.validateAccessToken(); const { data } = await api.userApi.getMyUserInfo();
event.locals.user = data;
if (status === 201) { return await resolve(event);
event.locals.user = {
id,
accessToken,
firstName,
lastName,
isAdmin,
email
};
}
const response = await resolve(event);
return response;
} catch (error) { } catch (error) {
console.log('Error [handle]', error); event.locals.user = undefined;
return await resolve(event); return await resolve(event);
} }
}; };
@ -39,13 +26,6 @@ export const getSession: GetSession = async ({ locals }) => {
if (!locals.user) return {}; if (!locals.user) return {};
return { return {
user: { user: locals.user
id: locals.user.id,
accessToken: locals.user.accessToken,
firstName: locals.user.firstName,
lastName: locals.user.lastName,
isAdmin: locals.user.isAdmin,
email: locals.user.email
}
}; };
}; };

View file

@ -1,64 +0,0 @@
type AdminRegistrationResult = Promise<{
error?: string;
success?: string;
user?: {
email: string;
};
}>;
type LoginResult = Promise<{
error?: string;
success?: string;
user?: {
accessToken: string;
firstName: string;
lastName: string;
isAdmin: boolean;
id: string;
email: string;
shouldChangePassword: boolean;
};
}>;
type UpdateResult = Promise<{
error?: string;
success?: string;
user?: {
accessToken: string;
firstName: string;
lastName: string;
isAdmin: boolean;
id: string;
email: string;
};
}>;
export async function sendRegistrationForm(form: HTMLFormElement): AdminRegistrationResult {
const response = await fetch(form.action, {
method: form.method,
body: new FormData(form),
headers: { accept: 'application/json' },
});
return await response.json();
}
export async function sendLoginForm(form: HTMLFormElement): LoginResult {
const response = await fetch(form.action, {
method: form.method,
body: new FormData(form),
headers: { accept: 'application/json' },
});
return await response.json();
}
export async function sendUpdateForm(form: HTMLFormElement): UpdateResult {
const response = await fetch(form.action, {
method: form.method,
body: new FormData(form),
headers: { accept: 'application/json' },
});
return await response.json();
}

View file

@ -36,6 +36,7 @@
let backUrl = '/albums'; let backUrl = '/albums';
let currentAlbumName = ''; let currentAlbumName = '';
let currentUser: UserResponseDto; let currentUser: UserResponseDto;
let titleInput: HTMLInputElement;
$: isOwned = currentUser?.id == album.ownerId; $: isOwned = currentUser?.id == album.ownerId;
@ -298,6 +299,12 @@
<section class="m-auto my-[160px] w-[60%]"> <section class="m-auto my-[160px] w-[60%]">
<input <input
on:keydown={(e) => {
if (e.key == 'Enter') {
isEditingTitle = false;
titleInput.blur();
}
}}
on:focus={() => (isEditingTitle = true)} on:focus={() => (isEditingTitle = true)}
on:blur={() => (isEditingTitle = false)} on:blur={() => (isEditingTitle = false)}
class={`transition-all text-6xl text-immich-primary w-[99%] border-b-2 border-transparent outline-none ${ class={`transition-all text-6xl text-immich-primary w-[99%] border-b-2 border-transparent outline-none ${
@ -306,6 +313,7 @@
type="text" type="text"
bind:value={album.albumName} bind:value={album.albumName}
disabled={!isOwned} disabled={!isOwned}
bind:this={titleInput}
/> />
{#if album.assets.length > 0} {#if album.assets.length > 0}

View file

@ -6,7 +6,6 @@
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte'; import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
import PhotoViewer from './photo-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte';
import DetailPanel from './detail-panel.svelte'; import DetailPanel from './detail-panel.svelte';
import { session } from '$app/stores';
import { downloadAssets } from '$lib/stores/download'; import { downloadAssets } from '$lib/stores/download';
import VideoViewer from './video-viewer.svelte'; import VideoViewer from './video-viewer.svelte';
import { api, AssetResponseDto, AssetTypeEnum } from '@api'; import { api, AssetResponseDto, AssetTypeEnum } from '@api';
@ -62,7 +61,6 @@
}; };
const downloadFile = async () => { const downloadFile = async () => {
if ($session.user) {
try { try {
const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id; const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
const imageExtension = asset.originalPath.split('.')[1]; const imageExtension = asset.originalPath.split('.')[1];
@ -120,7 +118,6 @@
} catch (e) { } catch (e) {
console.log('Error downloading file ', e); console.log('Error downloading file ', e);
} }
}
}; };
</script> </script>

View file

@ -37,7 +37,7 @@
map = leaflet.map('map'); map = leaflet.map('map');
leaflet leaflet
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { .tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}) })
.addTo(map); .addTo(map);
} }
@ -124,7 +124,7 @@
{moment( {moment(
asset.exifInfo.dateTimeOriginal asset.exifInfo.dateTimeOriginal
.toString() .toString()
.slice(0, asset.exifInfo.dateTimeOriginal.toString().length - 1), .slice(0, asset.exifInfo.dateTimeOriginal.toString().length - 1)
).format('ddd, hh:mm A')} ).format('ddd, hh:mm A')}
</p> </p>
<p>GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}</p> <p>GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}</p>
@ -141,7 +141,9 @@
<div class="flex text-sm gap-2"> <div class="flex text-sm gap-2">
{#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth} {#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth}
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} {#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
<p>{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}MP</p> <p>
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}MP
</p>
{/if} {/if}
<p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p> <p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p>

View file

@ -14,9 +14,14 @@
<div class="mb-2" transition:slide> <div class="mb-2" transition:slide>
<p class="font-medium text-xs truncate">{fileName}</p> <p class="font-medium text-xs truncate">{fileName}</p>
<div class="flex flex-row-reverse place-items-center gap-5"> <div class="flex flex-row-reverse place-items-center gap-5">
<p><span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100</p> <p>
<span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100
</p>
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700"> <div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700">
<div class="bg-immich-primary h-[7px] rounded-full" style={`width: ${$downloadAssets[fileName]}%`} /> <div
class="bg-immich-primary h-[7px] rounded-full"
style={`width: ${$downloadAssets[fileName]}%`}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -22,8 +22,8 @@
} }
}, },
{ {
rootMargin, rootMargin
}, }
); );
observer.observe(container); observer.observe(container);

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { session } from '$app/stores';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
@ -14,14 +13,11 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
onMount(async () => { onMount(async () => {
if ($session.user) {
const { data } = await api.assetApi.getAssetById(assetId); const { data } = await api.assetApi.getAssetById(assetId);
assetInfo = data; assetInfo = data;
}
}); });
const loadAssetData = async () => { const loadAssetData = async () => {
if ($session.user) {
try { try {
const { data } = await api.assetApi.serveFile( const { data } = await api.assetApi.serveFile(
assetInfo.deviceAssetId, assetInfo.deviceAssetId,
@ -40,7 +36,6 @@
const assetData = URL.createObjectURL(data); const assetData = URL.createObjectURL(data);
return assetData; return assetData;
} catch (e) {} } catch (e) {}
}
}; };
</script> </script>

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { session } from '$app/stores';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
@ -16,19 +15,16 @@
let isVideoLoading = true; let isVideoLoading = true;
onMount(async () => { onMount(async () => {
if ($session.user) {
const { data: assetInfo } = await api.assetApi.getAssetById(assetId); const { data: assetInfo } = await api.assetApi.getAssetById(assetId);
asset = assetInfo; asset = assetInfo;
await loadVideoData(); await loadVideoData();
}
}); });
const loadVideoData = async () => { const loadVideoData = async () => {
isVideoLoading = true; isVideoLoading = true;
if ($session.user) {
try { try {
const { data } = await api.assetApi.serveFile( const { data } = await api.assetApi.serveFile(
asset.deviceAssetId, asset.deviceAssetId,
@ -59,7 +55,6 @@
return videoData; return videoData;
} catch (e) {} } catch (e) {}
}
}; };
</script> </script>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { sendRegistrationForm } from '$lib/auth-api'; import { api } from '@api';
let error: string; let error: string;
let success: string; let success: string;
@ -19,21 +19,33 @@
canRegister = true; canRegister = true;
} }
} }
async function registerAdmin(event: SubmitEvent) { async function registerAdmin(event: SubmitEvent) {
if (canRegister) { if (canRegister) {
error = ''; error = '';
const formElement = event.target as HTMLFormElement; const formElement = event.target as HTMLFormElement;
const response = await sendRegistrationForm(formElement); const form = new FormData(formElement);
if (response.error) { const email = form.get('email');
error = JSON.stringify(response.error); const password = form.get('password');
} const firstName = form.get('firstName');
const lastName = form.get('lastName');
if (response.success) { const { status } = await api.authenticationApi.adminSignUp({
success = response.success; email: String(email),
password: String(password),
firstName: String(firstName),
lastName: String(lastName)
});
if (status === 201) {
goto('/auth/login'); goto('/auth/login');
return;
} else {
error = 'Error create admin account';
return;
} }
} }
} }
@ -44,8 +56,8 @@
<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" /> <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" />
<h1 class="text-2xl text-immich-primary font-medium">Admin Registration</h1> <h1 class="text-2xl text-immich-primary font-medium">Admin Registration</h1>
<p class="text-sm border rounded-md p-4 font-mono text-gray-600"> <p class="text-sm border rounded-md p-4 font-mono text-gray-600">
Since you are the first user on the system, you will be assigned as the Admin and are responsible for Since you are the first user on the system, you will be assigned as the Admin and are
administrative tasks, and additional users will be created by you. responsible for administrative tasks, and additional users will be created by you.
</p> </p>
</div> </div>
@ -57,7 +69,14 @@
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Admin Password</label> <label class="immich-form-label" for="password">Admin Password</label>
<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} /> <input
class="immich-form-input"
id="password"
name="password"
type="password"
required
bind:value={password}
/>
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { sendUpdateForm } from '$lib/auth-api'; import { api } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import type { ImmichUser } from '../../models/immich-user'; import type { ImmichUser } from '../../models/immich-user';
@ -21,24 +21,24 @@
changeChagePassword = true; changeChagePassword = true;
} }
} }
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function changePassword(event: SubmitEvent) { async function changePassword() {
if (changeChagePassword) { if (changeChagePassword) {
error = ''; error = '';
const formElement = event.target as HTMLFormElement; const { status } = await api.userApi.updateUser({
id: user.id,
const response = await sendUpdateForm(formElement); password: String(password),
shouldChangePassword: false
if (response.error) { });
error = JSON.stringify(response.error);
}
if (response.success) {
success = 'Password has been changed';
if (status === 200) {
dispatch('success'); dispatch('success');
return;
} else {
console.error('Error changing password');
} }
} }
} }
@ -54,15 +54,22 @@
{user.lastName} ({user.email}), {user.lastName} ({user.email}),
<br /> <br />
<br /> <br />
This is either the first time you are signing into the system or a request has been made to change your password. Please This is either the first time you are signing into the system or a request has been made to change
enter the new password below. your password. Please enter the new password below.
</p> </p>
</div> </div>
<form on:submit|preventDefault={changePassword} method="post" autocomplete="off"> <form on:submit|preventDefault={changePassword} method="post" autocomplete="off">
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">New Password</label> <label class="immich-form-label" for="password">New Password</label>
<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} /> <input
class="immich-form-input"
id="password"
name="password"
type="password"
required
bind:value={password}
/>
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { sendRegistrationForm } from '$lib/auth-api'; import { api } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
let error: string; let error: string;
@ -22,21 +22,33 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function registerUser(event: SubmitEvent) { async function registerUser(event: SubmitEvent) {
console.log('registerUser');
if (canCreateUser) { if (canCreateUser) {
error = ''; error = '';
const formElement = event.target as HTMLFormElement; const formElement = event.target as HTMLFormElement;
const response = await sendRegistrationForm(formElement); const form = new FormData(formElement);
if (response.error) { const email = form.get('email');
error = JSON.stringify(response.error); const password = form.get('password');
} const firstName = form.get('firstName');
const lastName = form.get('lastName');
if (response.success) { const { status } = await api.userApi.createUser({
email: String(email),
password: String(password),
firstName: String(firstName),
lastName: String(lastName)
});
if (status === 201) {
success = 'New user created'; success = 'New user created';
dispatch('user-created'); dispatch('user-created');
return;
} else {
error = 'Error create user account';
} }
} }
} }
@ -47,11 +59,12 @@
<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" /> <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" />
<h1 class="text-2xl text-immich-primary font-medium">Create new user</h1> <h1 class="text-2xl text-immich-primary font-medium">Create new user</h1>
<p class="text-sm border rounded-md p-4 font-mono text-gray-600"> <p class="text-sm border rounded-md p-4 font-mono text-gray-600">
Please provide your user with the password, they will have to change it on their first sign in. Please provide your user with the password, they will have to change it on their first sign
in.
</p> </p>
</div> </div>
<form on:submit|preventDefault={registerUser} method="post" action="/admin/api/create-user" autocomplete="off"> <form on:submit|preventDefault={registerUser} autocomplete="off">
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label> <label class="immich-form-label" for="email">Email</label>
<input class="immich-form-input" id="email" name="email" type="email" required /> <input class="immich-form-input" id="email" name="email" type="email" required />
@ -59,7 +72,14 @@
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label> <label class="immich-form-label" for="password">Password</label>
<input class="immich-form-input" id="password" name="password" type="password" required bind:value={password} /> <input
class="immich-form-input"
id="password"
name="password"
type="password"
required
bind:value={password}
/>
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">

View file

@ -1,41 +1,35 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import { session } from '$app/stores';
import { sendLoginForm } from '$lib/auth-api';
import { loginPageMessage } from '$lib/constants'; import { loginPageMessage } from '$lib/constants';
import { api } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
let error: string; let error: string;
let email: string = '';
let password: string = '';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function login(event: SubmitEvent) { const login = async () => {
try {
error = ''; error = '';
const formElement = event.target as HTMLFormElement; const { data } = await api.authenticationApi.login({
email,
password
});
const response = await sendLoginForm(formElement); if (!data.isAdmin && data.shouldChangePassword) {
dispatch('first-login');
if (response.error) { return;
error = response.error;
} }
if (response.success) { dispatch('success');
$session.user = { return;
accessToken: response.user!.accessToken, } catch (e) {
firstName: response.user!.firstName, error = 'Incorrect email or password';
lastName: response.user!.lastName, return;
isAdmin: response.user!.isAdmin, }
id: response.user!.id,
email: response.user!.email,
}; };
if (!response.user?.isAdmin && response.user?.shouldChangePassword) {
return dispatch('first-login');
}
return dispatch('success');
}
}
</script> </script>
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> <div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8">
@ -45,20 +39,36 @@
</div> </div>
{#if loginPageMessage} {#if loginPageMessage}
<p class="text-sm border rounded-md m-4 p-4 text-immich-primary font-medium bg-immich-primary/5"> <p
class="text-sm border rounded-md m-4 p-4 text-immich-primary font-medium bg-immich-primary/5"
>
{@html loginPageMessage} {@html loginPageMessage}
</p> </p>
{/if} {/if}
<form on:submit|preventDefault={login} method="post" action="" autocomplete="off"> <form on:submit|preventDefault={login} autocomplete="off">
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label> <label class="immich-form-label" for="email">Email</label>
<input class="immich-form-input" id="email" name="email" type="email" required /> <input
class="immich-form-input"
id="email"
name="email"
type="email"
bind:value={email}
required
/>
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label> <label class="immich-form-label" for="password">Password</label>
<input class="immich-form-input" id="password" name="password" type="password" required /> <input
class="immich-form-input"
id="password"
name="password"
type="password"
bind:value={password}
required
/>
</div> </div>
{#if error} {#if error}

View file

@ -24,23 +24,27 @@
<section class="max-h-[400px] overflow-y-auto"> <section class="max-h-[400px] overflow-y-auto">
<div class="font-thin"> <div class="font-thin">
Hi friend, there is a new release of <span class="font-immich-title text-immich-primary font-bold" Hi friend, there is a new release of <span
>IMMICH</span class="font-immich-title text-immich-primary font-bold">IMMICH</span
>, please take your time to visit the >, please take your time to visit the
<span class="underline font-medium" <span class="underline font-medium"
><a href="https://github.com/alextran1502/immich/releases/latest" target="_blank" rel="noopener noreferrer" ><a
>release note</a href="https://github.com/alextran1502/immich/releases/latest"
target="_blank"
rel="noopener noreferrer">release note</a
></span ></span
> >
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations, and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent
especially if you use WatchTower or any mechanism that handles updating your application automatically. any misconfigurations, especially if you use WatchTower or any mechanism that handles updating
your application automatically.
</div> </div>
{#if remoteVersion == 'v1.11.0_17-dev'} {#if remoteVersion == 'v1.11.0_17-dev'}
<div class="mt-2 font-thin"> <div class="mt-2 font-thin">
This specific version <span class="font-medium">v1.11.0_17-dev</span> includes changes in the docker-compose This specific version <span class="font-medium">v1.11.0_17-dev</span> includes changes in
setup that added additional containters. Please make sure to update the docker-compose file, pull new images the docker-compose setup that added additional containters. Please make sure to update the
and check your setup for the latest features and bug fixes. docker-compose file, pull new images and check your setup for the latest features and bug
fixes.
</div> </div>
{/if} {/if}
</section> </section>

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { session } from '$app/stores';
import { createEventDispatcher, onDestroy } from 'svelte'; import { createEventDispatcher, onDestroy } from 'svelte';
import { fade, fly } from 'svelte/transition'; import { fade, fly } from 'svelte/transition';
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte'; import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
@ -32,7 +31,6 @@
let videoAbortController: AbortController; let videoAbortController: AbortController;
const loadImageData = async () => { const loadImageData = async () => {
if ($session.user) {
const { data } = await api.assetApi.getAssetThumbnail(asset.id, format, { const { data } = await api.assetApi.getAssetThumbnail(asset.id, format, {
responseType: 'blob' responseType: 'blob'
}); });
@ -40,7 +38,6 @@
imageData = URL.createObjectURL(data); imageData = URL.createObjectURL(data);
return imageData; return imageData;
} }
}
}; };
const loadVideoData = async () => { const loadVideoData = async () => {

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { session } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import type { ImmichUser } from '$lib/models/immich-user'; import type { ImmichUser } from '$lib/models/immich-user';
@ -23,7 +22,6 @@
}); });
const getUserProfileImage = async () => { const getUserProfileImage = async () => {
if ($session.user) {
try { try {
await api.userApi.getProfileImage(user.id); await api.userApi.getProfileImage(user.id);
shouldShowProfileImage = true; shouldShowProfileImage = true;
@ -31,7 +29,6 @@
console.log('User does not have a profile image'); console.log('User does not have a profile image');
shouldShowProfileImage = false; shouldShowProfileImage = false;
} }
}
}; };
const getFirstLetter = (text?: string) => { const getFirstLetter = (text?: string) => {
return text?.charAt(0).toUpperCase(); return text?.charAt(0).toUpperCase();

View file

@ -1,49 +1,50 @@
<script lang="ts"> <script lang="ts">
import { getRequest } from '$lib/utils/api-helper';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { serverEndpoint } from '$lib/constants'; import { serverEndpoint } from '$lib/constants';
import Cloud from 'svelte-material-icons/Cloud.svelte'; import Cloud from 'svelte-material-icons/Cloud.svelte';
import Dns from 'svelte-material-icons/Dns.svelte'; import Dns from 'svelte-material-icons/Dns.svelte';
import LoadingSpinner from './loading-spinner.svelte'; import LoadingSpinner from './loading-spinner.svelte';
import { goto } from '$app/navigation'; import { api, ServerInfoResponseDto } from '@api';
type ServerInfoType = {
diskAvailable: string;
diskAvailableRaw: number;
diskSize: string;
diskSizeRaw: number;
diskUsagePercentage: number;
diskUse: string;
diskUseRaw: number;
};
let endpoint = serverEndpoint; let endpoint = serverEndpoint;
let isServerOk = true; let isServerOk = true;
let serverVersion = ''; let serverVersion = '';
let serverInfoRes: ServerInfoType; let serverInfo: ServerInfoResponseDto;
onMount(async () => { onMount(async () => {
const res = await getRequest('server-info/version', ''); try {
serverVersion = `v${res.major}.${res.minor}.${res.patch}`; const { data: version } = await api.serverInfoApi.getServerVersion();
serverInfoRes = (await getRequest('server-info', '')) as ServerInfoType; serverVersion = `v${version.major}.${version.minor}.${version.patch}`;
const { data: serverInfoRes } = await api.serverInfoApi.getServerInfo();
serverInfo = serverInfoRes;
getStorageUsagePercentage(); getStorageUsagePercentage();
} catch (e) {
console.log('Error [StatusBox] [onMount]');
isServerOk = false;
}
}); });
const pingServerInterval = setInterval(async () => { const pingServerInterval = setInterval(async () => {
const response = await getRequest('server-info/ping', ''); try {
const { data: pingReponse } = await api.serverInfoApi.pingServer();
if (response.res === 'pong') isServerOk = true; if (pingReponse.res === 'pong') isServerOk = true;
else isServerOk = false; else isServerOk = false;
serverInfoRes = (await getRequest('server-info', '')) as ServerInfoType; const { data: serverInfoRes } = await api.serverInfoApi.getServerInfo();
serverInfo = serverInfoRes;
} catch (e) {
console.log('Error [StatusBox] [pingServerInterval]');
isServerOk = false;
}
}, 10000); }, 10000);
onDestroy(() => clearInterval(pingServerInterval)); onDestroy(() => clearInterval(pingServerInterval));
const getStorageUsagePercentage = () => { const getStorageUsagePercentage = () => {
return Math.round((serverInfoRes.diskUseRaw / serverInfoRes.diskSizeRaw) * 100); return Math.round((serverInfo?.diskUseRaw / serverInfo?.diskSizeRaw) * 100);
}; };
</script> </script>
@ -54,12 +55,15 @@
</div> </div>
<div> <div>
<p class="text-sm font-medium text-immich-primary">Storage</p> <p class="text-sm font-medium text-immich-primary">Storage</p>
{#if serverInfoRes} {#if serverInfo}
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700 my-2"> <div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700 my-2">
<!-- style={`width: ${$downloadAssets[fileName]}%`} --> <!-- style={`width: ${$downloadAssets[fileName]}%`} -->
<div class="bg-immich-primary h-[7px] rounded-full" style={`width: ${getStorageUsagePercentage()}%`} /> <div
class="bg-immich-primary h-[7px] rounded-full"
style={`width: ${getStorageUsagePercentage()}%`}
/>
</div> </div>
<p class="text-xs">{serverInfoRes?.diskUse} of {serverInfoRes?.diskSize} used</p> <p class="text-xs">{serverInfo?.diskUse} of {serverInfo?.diskSize} used</p>
{:else} {:else}
<div class="mt-2"> <div class="mt-2">
<LoadingSpinner /> <LoadingSpinner />

View file

@ -7,7 +7,6 @@
import type { UploadAsset } from '$lib/models/upload-asset'; import type { UploadAsset } from '$lib/models/upload-asset';
import { getAssetsInfo } from '$lib/stores/assets'; import { getAssetsInfo } from '$lib/stores/assets';
import { session } from '$app/stores'; import { session } from '$app/stores';
let showDetail = true; let showDetail = true;
let uploadLength = 0; let uploadLength = 0;
@ -75,12 +74,9 @@
} }
let isUploading = false; let isUploading = false;
uploadAssetsStore.isUploading.subscribe((value) => { uploadAssetsStore.isUploading.subscribe((value) => {
isUploading = value; isUploading = value;
if (isUploading == false) {
getAssetsInfo();
}
}); });
</script> </script>
@ -88,6 +84,7 @@
<div <div
in:fade={{ duration: 250 }} in:fade={{ duration: 250 }}
out:fade={{ duration: 250, delay: 1000 }} out:fade={{ duration: 250, delay: 1000 }}
on:outroend={() => getAssetsInfo()}
class="absolute right-6 bottom-6 z-[10000]" class="absolute right-6 bottom-6 z-[10000]"
> >
{#if showDetail} {#if showDetail}
@ -107,6 +104,7 @@
<div class="max-h-[400px] overflow-y-auto pr-2 rounded-lg immich-scrollbar"> <div class="max-h-[400px] overflow-y-auto pr-2 rounded-lg immich-scrollbar">
{#each $uploadAssetsStore as uploadAsset} {#each $uploadAssetsStore as uploadAsset}
{#key uploadAsset.id}
<div <div
in:fade={{ duration: 250 }} in:fade={{ duration: 250 }}
out:fade={{ duration: 100 }} out:fade={{ duration: 100 }}
@ -150,6 +148,7 @@
</div> </div>
</div> </div>
</div> </div>
{/key}
{/each} {/each}
</div> </div>
</div> </div>

View file

@ -2,12 +2,10 @@ import { writable, derived } from 'svelte/store';
export const downloadAssets = writable<Record<string, number>>({}); export const downloadAssets = writable<Record<string, number>>({});
export const isDownloading = derived(downloadAssets, ($downloadAssets) => { export const isDownloading = derived(downloadAssets, ($downloadAssets) => {
if (Object.keys($downloadAssets).length == 0) { if (Object.keys($downloadAssets).length == 0) {
return false; return false;
} }
return true; return true;
}) });

View file

@ -20,7 +20,7 @@ function createUploadStore() {
if (asset.id == id) { if (asset.id == id) {
return { return {
...asset, ...asset,
progress: progress, progress: progress
}; };
} }
@ -38,7 +38,7 @@ function createUploadStore() {
isUploading, isUploading,
addNewUploadAsset, addNewUploadAsset,
updateProgress, updateProgress,
removeUploadAsset, removeUploadAsset
}; };
} }

View file

@ -4,19 +4,16 @@ import { serverEndpoint } from '../constants';
let websocket: Socket; let websocket: Socket;
export const openWebsocketConnection = (accessToken: string) => { export const openWebsocketConnection = () => {
const websocketEndpoint = serverEndpoint.replace('/api', ''); const websocketEndpoint = serverEndpoint.replace('/api', '');
try { try {
websocket = io(websocketEndpoint, { websocket = io('', {
path: '/api/socket.io', path: '/api/socket.io',
transports: ['polling'], transports: ['polling'],
reconnection: true, reconnection: true,
forceNew: true, forceNew: true,
autoConnect: true, autoConnect: true
extraHeaders: {
Authorization: 'Bearer ' + accessToken,
},
}); });
listenToEvent(websocket); listenToEvent(websocket);

View file

@ -1,59 +0,0 @@
import { serverEndpoint } from '../constants';
type ISend = {
method: string;
path: string;
data?: any;
token: string;
customHeaders?: Record<string, string>;
};
type IOption = {
method: string;
headers: Record<string, string>;
body: any;
};
async function send({ method, path, data, token, customHeaders }: ISend) {
const opts: IOption = { method, headers: {} } as IOption;
if (data) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(data);
}
if (customHeaders) {
console.log(customHeaders);
// opts.headers[customHeader.$1]
}
if (token) {
opts.headers['Authorization'] = `Bearer ${token}`;
}
return fetch(`${serverEndpoint}/${path}`, opts)
.then((r) => r.text())
.then((json) => {
try {
return JSON.parse(json);
} catch (err) {
return json;
}
});
}
export function getRequest(path: string, token: string, customHeaders?: Record<string, string>) {
return send({ method: 'GET', path, token, customHeaders });
}
export function delRequest(path: string, token: string, customHeaders?: Record<string, string>) {
return send({ method: 'DELETE', path, token, customHeaders });
}
export function postRequest(path: string, data: any, token: string, customHeaders?: Record<string, string>) {
return send({ method: 'POST', path, data, token, customHeaders });
}
export function putRequest(path: string, data: any, token: string, customHeaders?: Record<string, string>) {
return send({ method: 'PUT', path, data, token, customHeaders });
}

View file

@ -11,8 +11,8 @@ type GithubRelease = {
export const checkAppVersion = async (): Promise<CheckAppVersionReponse> => { export const checkAppVersion = async (): Promise<CheckAppVersionReponse> => {
const res = await fetch('https://api.github.com/repos/alextran1502/immich/releases/latest', { const res = await fetch('https://api.github.com/repos/alextran1502/immich/releases/latest', {
headers: { headers: {
Accept: 'application/vnd.github.v3+json', Accept: 'application/vnd.github.v3+json'
}, }
}); });
if (res.status == 200) { if (res.status == 200) {
@ -23,7 +23,7 @@ export const checkAppVersion = async (): Promise<CheckAppVersionReponse> => {
return { return {
shouldShowAnnouncement: true, shouldShowAnnouncement: true,
remoteVersion: latestRelease.tag_name, remoteVersion: latestRelease.tag_name,
localVersion: 'empty', localVersion: 'empty'
}; };
} }
@ -31,20 +31,20 @@ export const checkAppVersion = async (): Promise<CheckAppVersionReponse> => {
return { return {
shouldShowAnnouncement: true, shouldShowAnnouncement: true,
remoteVersion: latestRelease.tag_name, remoteVersion: latestRelease.tag_name,
localVersion: appVersion, localVersion: appVersion
}; };
} }
return { return {
shouldShowAnnouncement: false, shouldShowAnnouncement: false,
remoteVersion: latestRelease.tag_name, remoteVersion: latestRelease.tag_name,
localVersion: appVersion, localVersion: appVersion
}; };
} else { } else {
return { return {
shouldShowAnnouncement: false, shouldShowAnnouncement: false,
remoteVersion: '0', remoteVersion: '0',
localVersion: '0', localVersion: '0'
}; };
} }
}; };

View file

@ -10,6 +10,6 @@ export function clickOutside(node: Node) {
return { return {
destroy() { destroy() {
document.removeEventListener('click', handleClick, true); document.removeEventListener('click', handleClick, true);
}, }
}; };
} }

View file

@ -5,7 +5,7 @@ import { uploadAssetsStore } from '$lib/stores/upload';
import type { UploadAsset } from '../models/upload-asset'; import type { UploadAsset } from '../models/upload-asset';
import { api } from '@api'; import { api } from '@api';
export async function fileUploader(asset: File, accessToken: string) { export async function fileUploader(asset: File) {
const assetType = asset.type.split('/')[0].toUpperCase(); const assetType = asset.type.split('/')[0].toUpperCase();
const temp = asset.name.split('.'); const temp = asset.name.split('.');
const fileExtension = temp[temp.length - 1]; const fileExtension = temp[temp.length - 1];
@ -56,7 +56,7 @@ export async function fileUploader(asset: File, accessToken: string) {
const { data, status } = await api.assetApi.checkDuplicateAsset({ const { data, status } = await api.assetApi.checkDuplicateAsset({
deviceAssetId: String(deviceAssetId), deviceAssetId: String(deviceAssetId),
deviceId: 'WEB', deviceId: 'WEB'
}); });
if (status === 200) { if (status === 200) {
@ -72,7 +72,7 @@ export async function fileUploader(asset: File, accessToken: string) {
id: deviceAssetId, id: deviceAssetId,
file: asset, file: asset,
progress: 0, progress: 0,
fileExtension: fileExtension, fileExtension: fileExtension
}; };
uploadAssetsStore.addNewUploadAsset(newUploadAsset); uploadAssetsStore.addNewUploadAsset(newUploadAsset);
@ -101,7 +101,6 @@ export async function fileUploader(asset: File, accessToken: string) {
}; };
request.open('POST', `${serverEndpoint}/asset/upload`); request.open('POST', `${serverEndpoint}/asset/upload`);
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
request.send(formData); request.send(formData);
} catch (e) { } catch (e) {

View file

@ -2,11 +2,7 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { checkAppVersion } from '$lib/utils/check-app-version'; import { checkAppVersion } from '$lib/utils/check-app-version';
export const load: Load = async ({ url, session }) => { export const load: Load = async ({ url }) => {
if (session.user) {
api.setAccessToken(session.user.accessToken);
}
return { return {
props: { url } props: { url }
}; };
@ -17,12 +13,10 @@
import '../app.css'; import '../app.css';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte'; import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
import AnnouncementBox from '$lib/components/shared-components/announcement-box.svelte'; import AnnouncementBox from '$lib/components/shared-components/announcement-box.svelte';
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte'; import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { api } from '@api';
export let url: string; export let url: string;
let shouldShowAnnouncement: boolean; let shouldShowAnnouncement: boolean;
@ -43,7 +37,9 @@
<div in:fade={{ duration: 100 }}> <div in:fade={{ duration: 100 }}>
<slot /> <slot />
<DownloadPanel /> <DownloadPanel />
<UploadPanel /> <UploadPanel />
{#if shouldShowAnnouncement} {#if shouldShowAnnouncement}
<AnnouncementBox <AnnouncementBox
{localVersion} {localVersion}

View file

@ -1,34 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { api } from '@api';
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 { status } = await api.userApi.createUser({
email: String(email),
password: String(password),
firstName: String(firstName),
lastName: String(lastName)
});
if (status === 201) {
return {
status: 201,
body: {
success: 'Succesfully create user account'
}
};
} else {
return {
status: 400,
body: {
error: 'Error create user account'
}
};
}
};

View file

@ -2,23 +2,24 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { api, UserResponseDto } from '@api'; import { api, UserResponseDto } from '@api';
export const load: Load = async ({ session }) => { export const load: Load = async () => {
if (!session.user) { try {
const { data: allUsers } = await api.userApi.getAllUsers(false);
const { data: user } = await api.userApi.getMyUserInfo();
return {
status: 200,
props: {
user: user,
allUsers: allUsers
}
};
} catch (e) {
return { return {
status: 302, status: 302,
redirect: '/auth/login' redirect: '/auth/login'
}; };
} }
const { data } = await api.userApi.getAllUsers(false);
return {
status: 200,
props: {
user: session.user,
allUsers: data
}
};
}; };
</script> </script>
@ -35,7 +36,7 @@
import CreateUserForm from '$lib/components/forms/create-user-form.svelte'; import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
import StatusBox from '$lib/components/shared-components/status-box.svelte'; import StatusBox from '$lib/components/shared-components/status-box.svelte';
let selectedAction: AdminSideBarSelection; let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
export let user: ImmichUser; export let user: ImmichUser;
export let allUsers: UserResponseDto[]; export let allUsers: UserResponseDto[];

View file

@ -4,38 +4,39 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { AlbumResponseDto, api } from '@api'; import { AlbumResponseDto, api } from '@api';
export const load: Load = async ({ session, params }) => { export const load: Load = async ({ params }) => {
if (!session.user) { try {
return {
status: 302,
redirect: '/auth/login'
};
}
const albumId = params['albumId']; const albumId = params['albumId'];
let album: AlbumResponseDto; const { data: albumInfo } = await api.albumApi.getAlbumInfo(albumId);
try { return {
const { data } = await api.albumApi.getAlbumInfo(albumId); status: 200,
album = data; props: {
album: albumInfo
}
};
} catch (e) { } catch (e) {
if (e instanceof AxiosError) {
if (e.response?.status === 404) {
return { return {
status: 302, status: 302,
redirect: '/albums' redirect: '/albums'
}; };
} }
}
return { return {
status: 200, status: 302,
props: { redirect: '/auth/login'
album: album
}
}; };
}
}; };
</script> </script>
<script lang="ts"> <script lang="ts">
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte'; import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
import { AxiosError } from 'axios';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
</script> </script>

View file

@ -1,15 +1,19 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
export const prerender = false; export const prerender = false;
import { api } from '@api';
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ session, params }) => { export const load: Load = async ({ params }) => {
if (!session.user) { try {
await api.userApi.getMyUserInfo();
} catch (e) {
return { return {
status: 302, status: 302,
redirect: '/auth/login' redirect: '/auth/login'
}; };
} }
const albumId = params['albumId']; const albumId = params['albumId'];
if (albumId) { if (albumId) {

View file

@ -9,29 +9,24 @@
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte'; import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import { AlbumResponseDto, api } from '@api'; import { AlbumResponseDto, api } from '@api';
export const load: Load = async ({ session }) => { export const load: Load = async () => {
if (!session.user) { try {
const { data: user } = await api.userApi.getMyUserInfo();
const { data: albums } = await api.albumApi.getAllAlbums();
return {
status: 200,
props: {
user: user,
albums: albums
}
};
} catch (e) {
return { return {
status: 302, status: 302,
redirect: '/auth/login' redirect: '/auth/login'
}; };
} }
let albums: AlbumResponseDto[] = [];
try {
const { data } = await api.albumApi.getAllAlbums();
albums = data;
} catch (e) {
console.log('Error [getAllAlbums] ', e);
}
return {
status: 200,
props: {
user: session.user,
albums: albums
}
};
}; };
</script> </script>

View file

@ -3,14 +3,7 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ session }) => { export const load: Load = async () => {
if (!session.user) {
return {
status: 302,
redirect: '/auth/login',
};
}
try { try {
const { data: userInfo } = await api.userApi.getMyUserInfo(); const { data: userInfo } = await api.userApi.getMyUserInfo();
@ -18,20 +11,19 @@
return { return {
status: 200, status: 200,
props: { props: {
user: userInfo, user: userInfo
}, }
}; };
} else { } else {
return { return {
status: 302, status: 302,
redirect: '/photos', redirect: '/photos'
}; };
} }
} catch (e) { } catch (e) {
console.log('ERROR Getting user info', e);
return { return {
status: 302, status: 302,
redirect: '/photos', redirect: '/auth/login'
}; };
} }
}; };

View file

@ -1,38 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { api } from '@api';
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) {
return {
status: 401,
body: {
error: 'Unauthorized'
}
};
}
const form = await request.formData();
const password = form.get('password');
const { status } = await api.userApi.updateUser({
id: locals.user.id,
password: String(password),
shouldChangePassword: false
});
if (status === 200) {
return {
status: 200,
body: {
success: 'Succesfully change password'
}
};
} else {
return {
status: 400,
body: {
error: 'Error change password'
}
};
}
};

View file

@ -1,59 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as cookie from 'cookie';
import { api } from '@api';
export const POST: RequestHandler = async ({ request }) => {
const form = await request.formData();
const email = form.get('email');
const password = form.get('password');
try {
const { data: authUser } = await api.authenticationApi.login({
email: String(email),
password: String(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
}
)
}
};
} catch (error) {
return {
status: 400,
body: {
error: 'Incorrect email or password'
}
};
}
};

View file

@ -1,9 +1,15 @@
import { api } from '@api';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const POST: RequestHandler = async () => { export const POST: RequestHandler = async () => {
api.removeAccessToken();
return { return {
headers: { headers: {
'Set-Cookie': 'session=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' 'Set-Cookie': [
'immich_is_authenticated=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;',
'immich_access_token=delete; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
]
}, },
body: { body: {
ok: true ok: true

View file

@ -1,25 +1,19 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ session }) => { export const load: Load = async () => {
const { data } = await api.userApi.getUserCount(); const { data } = await api.userApi.getUserCount();
if (data.userCount != 0) { if (data.userCount != 0) {
// Admin has been registered, redirect to login // Admin has been registered, redirect to login
if (!session.user) {
return { return {
status: 302, status: 302,
redirect: '/auth/login', redirect: '/auth/login'
}; };
} else {
return {
status: 302,
redirect: '/photos',
};
}
} }
return {}; return {
status: 200
};
}; };
</script> </script>

View file

@ -1,34 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { api } from '@api';
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 { status } = await api.authenticationApi.adminSignUp({
email: String(email),
password: String(password),
firstName: String(firstName),
lastName: String(lastName)
});
if (status === 201) {
return {
status: 201,
body: {
success: 'Succesfully create admin account'
}
};
} else {
return {
status: 400,
body: {
error: 'Error create admin account'
}
};
}
};

View file

@ -3,21 +3,23 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { api } from '@api'; import { api } from '@api';
export const load: Load = async ({ session }) => { export const load: Load = async () => {
const { data } = await api.userApi.getUserCount(); try {
const { data: user } = await api.userApi.getMyUserInfo();
if (session.user) {
return { return {
status: 302, status: 302,
redirect: '/photos', redirect: '/photos'
}; };
} } catch (e) {}
const { data } = await api.userApi.getUserCount();
return { return {
status: 200, status: 200,
props: { props: {
isAdminUserExist: data.userCount == 0 ? false : true, isAdminUserExist: data.userCount == 0 ? false : true
}, }
}; };
}; };
</script> </script>

View file

@ -1,19 +1,21 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
export const prerender = false; export const prerender = false;
import { api } from '@api';
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ session }) => { export const load: Load = async () => {
if (!session.user) { try {
await api.userApi.getMyUserInfo();
return { return {
status: 302, status: 302,
redirect: '/auth/login', redirect: '/photos'
};
} catch (e) {
return {
status: 302,
redirect: '/auth/login'
}; };
} }
return {
status: 302,
redirect: '/photos',
};
}; };
</script> </script>

View file

@ -4,41 +4,39 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { getAssetsInfo } from '$lib/stores/assets'; import { getAssetsInfo } from '$lib/stores/assets';
export const load: Load = async ({ session }) => { export const load: Load = async () => {
if (!session.user) { try {
return { const { data } = await api.userApi.getMyUserInfo();
status: 302,
redirect: '/auth/login'
};
}
await getAssetsInfo(); await getAssetsInfo();
return { return {
status: 200, status: 200,
props: { props: {
user: session.user user: data
} }
}; };
} catch (e) {
return {
status: 302,
redirect: '/auth/login'
};
}
}; };
</script> </script>
<script lang="ts"> <script lang="ts">
import type { ImmichUser } from '$lib/models/immich-user';
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte'; import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte'; import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { session } from '$app/stores';
import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets'; import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte'; import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
import moment from 'moment'; import moment from 'moment';
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte'; import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
import { fileUploader } from '$lib/utils/file-uploader'; import { fileUploader } from '$lib/utils/file-uploader';
import { AssetResponseDto } from '@api'; import { api, AssetResponseDto, UserResponseDto } from '@api';
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte'; import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
export let user: ImmichUser; export let user: UserResponseDto;
let selectedGroupThumbnail: number | null; let selectedGroupThumbnail: number | null;
let isMouseOverGroup: boolean; let isMouseOverGroup: boolean;
@ -67,7 +65,6 @@
}; };
const uploadClickedHandler = async () => { const uploadClickedHandler = async () => {
if ($session.user) {
try { try {
let fileSelector = document.createElement('input'); let fileSelector = document.createElement('input');
@ -83,7 +80,7 @@
); );
for (const asset of acceptedFile) { for (const asset of acceptedFile) {
await fileUploader(asset, $session.user!.accessToken); await fileUploader(asset);
} }
}; };
@ -91,7 +88,6 @@
} catch (e) { } catch (e) {
console.log('Error seelcting file', e); console.log('Error seelcting file', e);
} }
}
}; };
const navigateAssetForward = () => { const navigateAssetForward = () => {

View file

@ -4,30 +4,36 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { AlbumResponseDto, api, UserResponseDto } from '@api'; import { AlbumResponseDto, api, UserResponseDto } from '@api';
export const load: Load = async ({ session }) => { export const load: Load = async () => {
if (!session.user) { try {
const { data: user } = await api.userApi.getMyUserInfo();
const { data: sharedAlbums } = await api.albumApi.getAllAlbums(true);
return {
status: 200,
props: {
user: user,
sharedAlbums: sharedAlbums
}
};
} catch (e) {
return { return {
status: 302, status: 302,
redirect: '/auth/login' redirect: '/auth/login'
}; };
} }
let sharedAlbums: AlbumResponseDto[] = [];
try {
const { data } = await api.albumApi.getAllAlbums(true);
sharedAlbums = data;
} catch (e) {
console.log('Error [getAllAlbums] ', e);
}
return {
status: 200,
props: {
user: session.user,
sharedAlbums: sharedAlbums
}
};
}; };
</script>
<script lang="ts">
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
import { goto } from '$app/navigation';
export let user: UserResponseDto;
export let sharedAlbums: AlbumResponseDto[];
const createSharedAlbum = async () => { const createSharedAlbum = async () => {
try { try {
@ -40,28 +46,6 @@
console.log('Error [createAlbum] ', e); console.log('Error [createAlbum] ', e);
} }
}; };
const deleteAlbum = async (album: AlbumResponseDto) => {
try {
await api.albumApi.deleteAlbum(album.id);
return true;
} catch (e) {
console.log('Error [deleteAlbum] ', e);
return false;
}
};
</script>
<script lang="ts">
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
import AlbumCard from '$lib/components/album-page/album-card.svelte';
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
import { goto } from '$app/navigation';
export let user: UserResponseDto;
export let sharedAlbums: AlbumResponseDto[];
</script> </script>
<svelte:head> <svelte:head>

View file

@ -5,15 +5,15 @@ module.exports = {
colors: { colors: {
'immich-primary': '#4250af', 'immich-primary': '#4250af',
'immich-bg': '#f6f8fe', 'immich-bg': '#f6f8fe',
'immich-fg': 'black', 'immich-fg': 'black'
// 'immich-bg': '#121212', // 'immich-bg': '#121212',
// 'immich-fg': '#D0D0D0', // 'immich-fg': '#D0D0D0',
}, },
fontFamily: { fontFamily: {
'immich-title': ['Snowburst One', 'cursive'], 'immich-title': ['Snowburst One', 'cursive']
}
}
}, },
}, plugins: []
},
plugins: [],
}; };

View file

@ -5,10 +5,7 @@
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"lib": [ "lib": ["es2020", "DOM"],
"es2020",
"DOM"
],
"moduleResolution": "node", "moduleResolution": "node",
"module": "es2020", "module": "es2020",
"resolveJsonModule": true, "resolveJsonModule": true,
@ -19,15 +16,9 @@
"importsNotUsedAsValues": "preserve", "importsNotUsedAsValues": "preserve",
"preserveValueImports": false, "preserveValueImports": false,
"paths": { "paths": {
"$lib": [ "$lib": ["src/lib"],
"src/lib" "$lib/*": ["src/lib/*"],
], "@api": ["src/api"]
"$lib/*": [ }
"src/lib/*"
],
"@api": [
"src/api"
]
} }
},
} }