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
pull/378/head
Alex 2022-07-26 12:28:07 +07: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 5088 additions and 4674 deletions

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

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

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

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

File diff suppressed because it is too large Load Diff

@ -5,30 +5,29 @@
* Immich API
*
* The version of the OpenAPI document: 1.17.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { Configuration } from "./configuration";
import { Configuration } from './configuration';
// Some imports not used depending on template conditions
// @ts-ignore
import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
export const BASE_PATH = "/api".replace(/\/+$/, "");
export const BASE_PATH = '/api'.replace(/\/+$/, '');
/**
*
* @export
*/
export const COLLECTION_FORMATS = {
csv: ",",
ssv: " ",
tsv: "\t",
pipes: "|",
csv: ',',
ssv: ' ',
tsv: '\t',
pipes: '|'
};
/**
@ -37,8 +36,8 @@ export const COLLECTION_FORMATS = {
* @interface RequestArgs
*/
export interface RequestArgs {
url: string;
options: AxiosRequestConfig;
url: string;
options: AxiosRequestConfig;
}
/**
@ -47,15 +46,19 @@ export interface RequestArgs {
* @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) {
if (configuration) {
this.configuration = configuration;
this.basePath = configuration.basePath || this.basePath;
}
}
};
constructor(
configuration?: Configuration,
protected basePath: string = BASE_PATH,
protected axios: AxiosInstance = globalAxios
) {
if (configuration) {
this.configuration = configuration;
this.basePath = configuration.basePath || this.basePath;
}
}
}
/**
*
@ -64,8 +67,8 @@ export class BaseAPI {
* @extends {Error}
*/
export class RequiredError extends Error {
name: "RequiredError" = "RequiredError";
constructor(public field: string, msg?: string) {
super(msg);
}
name: 'RequiredError' = 'RequiredError';
constructor(public field: string, msg?: string) {
super(msg);
}
}

@ -5,134 +5,166 @@
* Immich API
*
* The version of the OpenAPI document: 1.17.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { Configuration } from "./configuration";
import { RequiredError, RequestArgs } from "./base";
import { Configuration } from './configuration';
import { RequiredError, RequestArgs } from './base';
import { AxiosInstance, AxiosResponse } from 'axios';
/**
*
* @export
*/
export const DUMMY_BASE_URL = 'https://example.com'
export const DUMMY_BASE_URL = 'https://example.com';
/**
*
* @throws {RequiredError}
* @export
*/
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
if (paramValue === null || paramValue === undefined) {
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
}
}
export const assertParamExists = function (
functionName: string,
paramName: string,
paramValue: unknown
) {
if (paramValue === null || paramValue === undefined) {
throw new RequiredError(
paramName,
`Required parameter ${paramName} was null or undefined when calling ${functionName}.`
);
}
};
/**
*
* @export
*/
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
if (configuration && configuration.apiKey) {
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
? await configuration.apiKey(keyParamName)
: await configuration.apiKey;
object[keyParamName] = localVarApiKeyValue;
}
}
export const setApiKeyToObject = async function (
object: any,
keyParamName: string,
configuration?: Configuration
) {
if (configuration && configuration.apiKey) {
const localVarApiKeyValue =
typeof configuration.apiKey === 'function'
? await configuration.apiKey(keyParamName)
: await configuration.apiKey;
object[keyParamName] = localVarApiKeyValue;
}
};
/**
*
* @export
*/
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
if (configuration && (configuration.username || configuration.password)) {
object["auth"] = { username: configuration.username, password: configuration.password };
}
}
if (configuration && (configuration.username || configuration.password)) {
object['auth'] = { username: configuration.username, password: configuration.password };
}
};
/**
*
* @export
*/
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
object["Authorization"] = "Bearer " + accessToken;
}
}
if (configuration && configuration.accessToken) {
const accessToken =
typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
object['Authorization'] = 'Bearer ' + accessToken;
}
};
/**
*
* @export
*/
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
? await configuration.accessToken(name, scopes)
: await configuration.accessToken;
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
}
}
export const setOAuthToObject = async function (
object: any,
name: string,
scopes: string[],
configuration?: Configuration
) {
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue =
typeof configuration.accessToken === 'function'
? await configuration.accessToken(name, scopes)
: await configuration.accessToken;
object['Authorization'] = 'Bearer ' + localVarAccessTokenValue;
}
};
/**
*
* @export
*/
export const setSearchParams = function (url: URL, ...objects: any[]) {
const searchParams = new URLSearchParams(url.search);
for (const object of objects) {
for (const key in object) {
if (Array.isArray(object[key])) {
searchParams.delete(key);
for (const item of object[key]) {
searchParams.append(key, item);
}
} else {
searchParams.set(key, object[key]);
}
}
}
url.search = searchParams.toString();
}
const searchParams = new URLSearchParams(url.search);
for (const object of objects) {
for (const key in object) {
if (Array.isArray(object[key])) {
searchParams.delete(key);
for (const item of object[key]) {
searchParams.append(key, item);
}
} else {
searchParams.set(key, object[key]);
}
}
}
url.search = searchParams.toString();
};
/**
*
* @export
*/
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
const nonString = typeof value !== 'string';
const needsSerialization = nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
: nonString;
return needsSerialization
? JSON.stringify(value !== undefined ? value : {})
: (value || "");
}
export const serializeDataIfNeeded = function (
value: any,
requestOptions: any,
configuration?: Configuration
) {
const nonString = typeof value !== 'string';
const needsSerialization =
nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
: nonString;
return needsSerialization ? JSON.stringify(value !== undefined ? value : {}) : value || '';
};
/**
*
* @export
*/
export const toPathString = function (url: URL) {
return url.pathname + url.search + url.hash
}
return url.pathname + url.search + url.hash;
};
/**
*
* @export
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, 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);
};
}
export const createRequestFunction = function (
axiosArgs: RequestArgs,
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);
};
};

@ -5,97 +5,117 @@
* Immich API
*
* The version of the OpenAPI document: 1.17.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
basePath?: string;
baseOptions?: any;
formDataCtor?: new () => any;
apiKey?:
| string
| Promise<string>
| ((name: string) => string)
| ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?:
| string
| Promise<string>
| ((name?: string, scopes?: string[]) => string)
| ((name?: string, scopes?: string[]) => Promise<string>);
basePath?: string;
baseOptions?: any;
formDataCtor?: new () => any;
}
export class Configuration {
/**
* parameter for apiKey security
* @param name security name
* @memberof Configuration
*/
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string;
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string;
/**
* parameter for oauth2 security
* @param name security name
* @param scopes oauth2 scope
* @memberof Configuration
*/
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
/**
* override base path
*
* @type {string}
* @memberof Configuration
*/
basePath?: string;
/**
* base options for axios calls
*
* @type {any}
* @memberof Configuration
*/
baseOptions?: any;
/**
* The FormData constructor that will be used to create multipart form data
* requests. You can inject this here so that execution environments that
* do not support the FormData class can still run the generated client.
*
* @type {new () => FormData}
*/
formDataCtor?: new () => any;
/**
* parameter for apiKey security
* @param name security name
* @memberof Configuration
*/
apiKey?:
| string
| Promise<string>
| ((name: string) => string)
| ((name: string) => Promise<string>);
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string;
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string;
/**
* parameter for oauth2 security
* @param name security name
* @param scopes oauth2 scope
* @memberof Configuration
*/
accessToken?:
| string
| Promise<string>
| ((name?: string, scopes?: string[]) => string)
| ((name?: string, scopes?: string[]) => Promise<string>);
/**
* override base path
*
* @type {string}
* @memberof Configuration
*/
basePath?: string;
/**
* base options for axios calls
*
* @type {any}
* @memberof Configuration
*/
baseOptions?: any;
/**
* The FormData constructor that will be used to create multipart form data
* requests. You can inject this here so that execution environments that
* do not support the FormData class can still run the generated client.
*
* @type {new () => FormData}
*/
formDataCtor?: new () => any;
constructor(param: ConfigurationParameters = {}) {
this.apiKey = param.apiKey;
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.basePath = param.basePath;
this.baseOptions = param.baseOptions;
this.formDataCtor = param.formDataCtor;
}
constructor(param: ConfigurationParameters = {}) {
this.apiKey = param.apiKey;
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.basePath = param.basePath;
this.baseOptions = param.baseOptions;
this.formDataCtor = param.formDataCtor;
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp(
'^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$',
'i'
);
return (
mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json')
);
}
}

@ -5,14 +5,12 @@
* Immich API
*
* The version of the OpenAPI document: 1.17.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export * from "./api";
export * from "./configuration";
export * from './api';
export * from './configuration';

31
web/src/app.d.ts vendored

@ -3,30 +3,15 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
interface Locals {
user?: {
id: string,
email: string,
accessToken: string,
firstName: string,
lastName: string,
isAdmin: boolean,
}
}
interface Locals {
user?: import('@api').UserResponseDto;
}
// interface Platform {}
// interface Platform {}
interface Session {
user?: {
id: string,
email: string,
accessToken: string,
firstName: string,
lastName: string
isAdmin: boolean,
}
}
interface Session {
user?: import('@api').UserResponseDto;
}
// interface Stuff {}
// interface Stuff {}
}

@ -1,15 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

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

@ -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();
}

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

@ -6,7 +6,6 @@
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
import PhotoViewer from './photo-viewer.svelte';
import DetailPanel from './detail-panel.svelte';
import { session } from '$app/stores';
import { downloadAssets } from '$lib/stores/download';
import VideoViewer from './video-viewer.svelte';
import { api, AssetResponseDto, AssetTypeEnum } from '@api';
@ -62,64 +61,62 @@
};
const downloadFile = async () => {
if ($session.user) {
try {
const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
const imageExtension = asset.originalPath.split('.')[1];
const imageFileName = imageName + '.' + imageExtension;
// If assets is already download -> return;
if ($downloadAssets[imageFileName]) {
return;
}
try {
const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
const imageExtension = asset.originalPath.split('.')[1];
const imageFileName = imageName + '.' + imageExtension;
$downloadAssets[imageFileName] = 0;
const { data, status } = await api.assetApi.downloadFile(
asset.deviceAssetId,
asset.deviceId,
false,
false,
{
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
const total = progressEvent.total;
const current = progressEvent.loaded;
let percentCompleted = Math.floor((current / total) * 100);
$downloadAssets[imageFileName] = percentCompleted;
}
// If assets is already download -> return;
if ($downloadAssets[imageFileName]) {
return;
}
$downloadAssets[imageFileName] = 0;
const { data, status } = await api.assetApi.downloadFile(
asset.deviceAssetId,
asset.deviceId,
false,
false,
{
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
const total = progressEvent.total;
const current = progressEvent.loaded;
let percentCompleted = Math.floor((current / total) * 100);
$downloadAssets[imageFileName] = percentCompleted;
}
}
);
if (!(data instanceof Blob)) {
return;
}
);
if (status === 200) {
const fileUrl = URL.createObjectURL(data);
const anchor = document.createElement('a');
anchor.href = fileUrl;
anchor.download = imageFileName;
if (!(data instanceof Blob)) {
return;
}
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
if (status === 200) {
const fileUrl = URL.createObjectURL(data);
const anchor = document.createElement('a');
anchor.href = fileUrl;
anchor.download = imageFileName;
URL.revokeObjectURL(fileUrl);
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
// Remove item from download list
setTimeout(() => {
const copy = $downloadAssets;
delete copy[imageFileName];
$downloadAssets = copy;
}, 2000);
}
} catch (e) {
console.log('Error downloading file ', e);
URL.revokeObjectURL(fileUrl);
// Remove item from download list
setTimeout(() => {
const copy = $downloadAssets;
delete copy[imageFileName];
$downloadAssets = copy;
}, 2000);
}
} catch (e) {
console.log('Error downloading file ', e);
}
};
</script>

@ -37,7 +37,7 @@
map = leaflet.map('map');
leaflet
.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);
}
@ -124,7 +124,7 @@
{moment(
asset.exifInfo.dateTimeOriginal
.toString()
.slice(0, asset.exifInfo.dateTimeOriginal.toString().length - 1),
.slice(0, asset.exifInfo.dateTimeOriginal.toString().length - 1)
).format('ddd, hh:mm A')}
</p>
<p>GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}</p>
@ -141,7 +141,9 @@
<div class="flex text-sm gap-2">
{#if 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}
<p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p>

@ -14,9 +14,14 @@
<div class="mb-2" transition:slide>
<p class="font-medium text-xs truncate">{fileName}</p>
<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="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>

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

@ -1,5 +1,4 @@
<script lang="ts">
import { session } from '$app/stores';
import { fade } from 'svelte/transition';
import { createEventDispatcher, onMount } from 'svelte';
@ -14,33 +13,29 @@
const dispatch = createEventDispatcher();
onMount(async () => {
if ($session.user) {
const { data } = await api.assetApi.getAssetById(assetId);
assetInfo = data;
}
const { data } = await api.assetApi.getAssetById(assetId);
assetInfo = data;
});
const loadAssetData = async () => {
if ($session.user) {
try {
const { data } = await api.assetApi.serveFile(
assetInfo.deviceAssetId,
deviceId,
false,
true,
{
responseType: 'blob'
}
);
if (!(data instanceof Blob)) {
return;
try {
const { data } = await api.assetApi.serveFile(
assetInfo.deviceAssetId,
deviceId,
false,
true,
{
responseType: 'blob'
}
);
if (!(data instanceof Blob)) {
return;
}
const assetData = URL.createObjectURL(data);
return assetData;
} catch (e) {}
}
const assetData = URL.createObjectURL(data);
return assetData;
} catch (e) {}
};
</script>

@ -1,5 +1,4 @@
<script lang="ts">
import { session } from '$app/stores';
import { fade } from 'svelte/transition';
import { createEventDispatcher, onMount } from 'svelte';
@ -16,50 +15,46 @@
let isVideoLoading = true;
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 () => {
isVideoLoading = true;
if ($session.user) {
try {
const { data } = await api.assetApi.serveFile(
asset.deviceAssetId,
asset.deviceId,
false,
true,
{
responseType: 'blob'
}
);
if (!(data instanceof Blob)) {
return;
try {
const { data } = await api.assetApi.serveFile(
asset.deviceAssetId,
asset.deviceId,
false,
true,
{
responseType: 'blob'
}
);
const videoData = URL.createObjectURL(data);
videoPlayerNode.src = videoData;
if (!(data instanceof Blob)) {
return;
}
videoPlayerNode.load();
const videoData = URL.createObjectURL(data);
videoPlayerNode.src = videoData;
videoPlayerNode.oncanplay = () => {
videoPlayerNode.muted = true;
videoPlayerNode.play();
videoPlayerNode.muted = false;
videoPlayerNode.load();
isVideoLoading = false;
};
videoPlayerNode.oncanplay = () => {
videoPlayerNode.muted = true;
videoPlayerNode.play();
videoPlayerNode.muted = false;
return videoData;
} catch (e) {}
}
isVideoLoading = false;
};
return videoData;
} catch (e) {}
};
</script>

@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { sendRegistrationForm } from '$lib/auth-api';
import { api } from '@api';
let error: string;
let success: string;
@ -19,21 +19,33 @@
canRegister = true;
}
}
async function registerAdmin(event: SubmitEvent) {
if (canRegister) {
error = '';
const formElement = event.target as HTMLFormElement;
const response = await sendRegistrationForm(formElement);
const form = new FormData(formElement);
if (response.error) {
error = JSON.stringify(response.error);
}
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 (response.success) {
success = response.success;
if (status === 201) {
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" />
<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">
Since you are the first user on the system, you will be assigned as the Admin and are responsible for
administrative tasks, and additional users will be created by you.
Since you are the first user on the system, you will be assigned as the Admin and are
responsible for administrative tasks, and additional users will be created by you.
</p>
</div>
@ -57,7 +69,14 @@
<div class="m-4 flex flex-col gap-2">
<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 class="m-4 flex flex-col gap-2">

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

@ -1,5 +1,5 @@
<script lang="ts">
import { sendRegistrationForm } from '$lib/auth-api';
import { api } from '@api';
import { createEventDispatcher } from 'svelte';
let error: string;
@ -22,21 +22,33 @@
const dispatch = createEventDispatcher();
async function registerUser(event: SubmitEvent) {
console.log('registerUser');
if (canCreateUser) {
error = '';
const formElement = event.target as HTMLFormElement;
const response = await sendRegistrationForm(formElement);
const form = new FormData(formElement);
if (response.error) {
error = JSON.stringify(response.error);
}
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 (response.success) {
if (status === 201) {
success = 'New 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" />
<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">
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>
</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">
<label class="immich-form-label" for="email">Email</label>
<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">
<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 class="m-4 flex flex-col gap-2">

@ -1,41 +1,35 @@
<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 { api } from '@api';
import { createEventDispatcher } from 'svelte';
let error: string;
const dispatch = createEventDispatcher();
async function login(event: SubmitEvent) {
error = '';
const formElement = event.target as HTMLFormElement;
let email: string = '';
let password: string = '';
const response = await sendLoginForm(formElement);
const dispatch = createEventDispatcher();
if (response.error) {
error = response.error;
}
const login = async () => {
try {
error = '';
if (response.success) {
$session.user = {
accessToken: response.user!.accessToken,
firstName: response.user!.firstName,
lastName: response.user!.lastName,
isAdmin: response.user!.isAdmin,
id: response.user!.id,
email: response.user!.email,
};
const { data } = await api.authenticationApi.login({
email,
password
});
if (!response.user?.isAdmin && response.user?.shouldChangePassword) {
return dispatch('first-login');
if (!data.isAdmin && data.shouldChangePassword) {
dispatch('first-login');
return;
}
return dispatch('success');
dispatch('success');
return;
} catch (e) {
error = 'Incorrect email or password';
return;
}
}
};
</script>
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8">
@ -45,20 +39,36 @@
</div>
{#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}
</p>
{/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">
<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 class="m-4 flex flex-col gap-2">
<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>
{#if error}

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

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

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

@ -1,49 +1,50 @@
<script lang="ts">
import { getRequest } from '$lib/utils/api-helper';
import { onDestroy, onMount } from 'svelte';
import { serverEndpoint } from '$lib/constants';
import Cloud from 'svelte-material-icons/Cloud.svelte';
import Dns from 'svelte-material-icons/Dns.svelte';
import LoadingSpinner from './loading-spinner.svelte';
import { goto } from '$app/navigation';
type ServerInfoType = {
diskAvailable: string;
diskAvailableRaw: number;
diskSize: string;
diskSizeRaw: number;
diskUsagePercentage: number;
diskUse: string;
diskUseRaw: number;
};
import { api, ServerInfoResponseDto } from '@api';
let endpoint = serverEndpoint;
let isServerOk = true;
let serverVersion = '';
let serverInfoRes: ServerInfoType;
let serverInfo: ServerInfoResponseDto;
onMount(async () => {
const res = await getRequest('server-info/version', '');
serverVersion = `v${res.major}.${res.minor}.${res.patch}`;
try {
const { data: version } = await api.serverInfoApi.getServerVersion();
serverInfoRes = (await getRequest('server-info', '')) as ServerInfoType;
serverVersion = `v${version.major}.${version.minor}.${version.patch}`;
getStorageUsagePercentage();
const { data: serverInfoRes } = await api.serverInfoApi.getServerInfo();
serverInfo = serverInfoRes;
getStorageUsagePercentage();
} catch (e) {
console.log('Error [StatusBox] [onMount]');
isServerOk = false;
}
});
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;
else isServerOk = false;
if (pingReponse.res === 'pong') isServerOk = true;
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);
onDestroy(() => clearInterval(pingServerInterval));
const getStorageUsagePercentage = () => {
return Math.round((serverInfoRes.diskUseRaw / serverInfoRes.diskSizeRaw) * 100);
return Math.round((serverInfo?.diskUseRaw / serverInfo?.diskSizeRaw) * 100);
};
</script>
@ -54,12 +55,15 @@
</div>
<div>
<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">
<!-- 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>
<p class="text-xs">{serverInfoRes?.diskUse} of {serverInfoRes?.diskSize} used</p>
<p class="text-xs">{serverInfo?.diskUse} of {serverInfo?.diskSize} used</p>
{:else}
<div class="mt-2">
<LoadingSpinner />

@ -7,7 +7,6 @@
import type { UploadAsset } from '$lib/models/upload-asset';
import { getAssetsInfo } from '$lib/stores/assets';
import { session } from '$app/stores';
let showDetail = true;
let uploadLength = 0;
@ -75,12 +74,9 @@
}
let isUploading = false;
uploadAssetsStore.isUploading.subscribe((value) => {
isUploading = value;
if (isUploading == false) {
getAssetsInfo();
}
});
</script>
@ -88,6 +84,7 @@
<div
in:fade={{ duration: 250 }}
out:fade={{ duration: 250, delay: 1000 }}
on:outroend={() => getAssetsInfo()}
class="absolute right-6 bottom-6 z-[10000]"
>
{#if showDetail}
@ -107,49 +104,51 @@
<div class="max-h-[400px] overflow-y-auto pr-2 rounded-lg immich-scrollbar">
{#each $uploadAssetsStore as uploadAsset}
<div
in:fade={{ duration: 250 }}
out:fade={{ duration: 100 }}
class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]"
>
<div class="relative">
<img
in:fade={{ duration: 250 }}
id={`${uploadAsset.id}`}
src="/immich-logo.svg"
alt=""
class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg "
/>
{#key uploadAsset.id}
<div
in:fade={{ duration: 250 }}
out:fade={{ duration: 100 }}
class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]"
>
<div class="relative">
<img
in:fade={{ duration: 250 }}
id={`${uploadAsset.id}`}
src="/immich-logo.svg"
alt=""
class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg "
/>
<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30">
<p
class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase"
>
.{uploadAsset.fileExtension}
</p>
<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30">
<p
class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase"
>
.{uploadAsset.fileExtension}
</p>
</div>
</div>
</div>
<div class="p-2 pr-4 flex flex-col justify-between">
<input
disabled
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
value={`[${getSizeInHumanReadableFormat(uploadAsset.file.size)}] ${
uploadAsset.file.name
}`}
/>
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
<div
class="bg-immich-primary h-[15px] rounded-md transition-all"
style={`width: ${uploadAsset.progress}%`}
<div class="p-2 pr-4 flex flex-col justify-between">
<input
disabled
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
value={`[${getSizeInHumanReadableFormat(uploadAsset.file.size)}] ${
uploadAsset.file.name
}`}
/>
<p class="absolute h-full w-full text-center top-0 text-[10px] ">
{uploadAsset.progress}/100
</p>
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
<div
class="bg-immich-primary h-[15px] rounded-md transition-all"
style={`width: ${uploadAsset.progress}%`}
/>
<p class="absolute h-full w-full text-center top-0 text-[10px] ">
{uploadAsset.progress}/100
</p>
</div>
</div>
</div>
</div>
{/key}
{/each}
</div>
</div>

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

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

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

@ -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 });
}

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

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

@ -5,7 +5,7 @@ import { uploadAssetsStore } from '$lib/stores/upload';
import type { UploadAsset } from '../models/upload-asset';
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 temp = asset.name.split('.');
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({
deviceAssetId: String(deviceAssetId),
deviceId: 'WEB',
deviceId: 'WEB'
});
if (status === 200) {
@ -72,7 +72,7 @@ export async function fileUploader(asset: File, accessToken: string) {
id: deviceAssetId,
file: asset,
progress: 0,
fileExtension: fileExtension,
fileExtension: fileExtension
};
uploadAssetsStore.addNewUploadAsset(newUploadAsset);
@ -101,7 +101,6 @@ export async function fileUploader(asset: File, accessToken: string) {
};
request.open('POST', `${serverEndpoint}/asset/upload`);
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
request.send(formData);
} catch (e) {

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

@ -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'
}
};
}
};

@ -2,23 +2,24 @@
import type { Load } from '@sveltejs/kit';
import { api, UserResponseDto } from '@api';
export const load: Load = async ({ session }) => {
if (!session.user) {
export const load: Load = async () => {
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 {
status: 302,
redirect: '/auth/login'
};
}
const { data } = await api.userApi.getAllUsers(false);
return {
status: 200,
props: {
user: session.user,
allUsers: data
}
};
};
</script>
@ -35,7 +36,7 @@
import CreateUserForm from '$lib/components/forms/create-user-form.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 allUsers: UserResponseDto[];

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

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

@ -9,29 +9,24 @@
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import { AlbumResponseDto, api } from '@api';
export const load: Load = async ({ session }) => {
if (!session.user) {
export const load: Load = async () => {
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 {
status: 302,
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>

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

@ -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'
}
};
}
};

@ -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'
}
};
}
};

@ -1,9 +1,15 @@
import { api } from '@api';
import type { RequestHandler } from '@sveltejs/kit';
export const POST: RequestHandler = async () => {
api.removeAccessToken();
return {
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: {
ok: true

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

@ -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'
}
};
}
};

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

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

@ -4,41 +4,39 @@
import type { Load } from '@sveltejs/kit';
import { getAssetsInfo } from '$lib/stores/assets';
export const load: Load = async ({ session }) => {
if (!session.user) {
export const load: Load = async () => {
try {
const { data } = await api.userApi.getMyUserInfo();
await getAssetsInfo();
return {
status: 200,
props: {
user: data
}
};
} catch (e) {
return {
status: 302,
redirect: '/auth/login'
};
}
await getAssetsInfo();
return {
status: 200,
props: {
user: session.user
}
};
};
</script>
<script lang="ts">
import type { ImmichUser } from '$lib/models/immich-user';
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import { fly } from 'svelte/transition';
import { session } from '$app/stores';
import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
import moment from 'moment';
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
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';
export let user: ImmichUser;
export let user: UserResponseDto;
let selectedGroupThumbnail: number | null;
let isMouseOverGroup: boolean;
@ -67,30 +65,28 @@
};
const uploadClickedHandler = async () => {
if ($session.user) {
try {
let fileSelector = document.createElement('input');
try {
let fileSelector = document.createElement('input');
fileSelector.type = 'file';
fileSelector.multiple = true;
fileSelector.accept = 'image/*,video/*,.heic,.heif';
fileSelector.type = 'file';
fileSelector.multiple = true;
fileSelector.accept = 'image/*,video/*,.heic,.heif';
fileSelector.onchange = async (e: any) => {
const files = Array.from<File>(e.target.files);
fileSelector.onchange = async (e: any) => {
const files = Array.from<File>(e.target.files);
const acceptedFile = files.filter(
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image'
);
const acceptedFile = files.filter(
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image'
);
for (const asset of acceptedFile) {
await fileUploader(asset, $session.user!.accessToken);
}
};
for (const asset of acceptedFile) {
await fileUploader(asset);
}
};
fileSelector.click();
} catch (e) {
console.log('Error seelcting file', e);
}
fileSelector.click();
} catch (e) {
console.log('Error seelcting file', e);
}
};

@ -4,30 +4,36 @@
import type { Load } from '@sveltejs/kit';
import { AlbumResponseDto, api, UserResponseDto } from '@api';
export const load: Load = async ({ session }) => {
if (!session.user) {
export const load: Load = async () => {
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 {
status: 302,
redirect: '/auth/login'
};
}
};
</script>
let sharedAlbums: AlbumResponseDto[] = [];
try {
const { data } = await api.albumApi.getAllAlbums(true);
sharedAlbums = data;
} catch (e) {
console.log('Error [getAllAlbums] ', e);
}
<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';
return {
status: 200,
props: {
user: session.user,
sharedAlbums: sharedAlbums
}
};
};
export let user: UserResponseDto;
export let sharedAlbums: AlbumResponseDto[];
const createSharedAlbum = async () => {
try {
@ -40,28 +46,6 @@
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>
<svelte:head>

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

@ -1,33 +1,24 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"es2020",
"DOM"
],
"moduleResolution": "node",
"module": "es2020",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "es2020",
"importsNotUsedAsValues": "preserve",
"preserveValueImports": false,
"paths": {
"$lib": [
"src/lib"
],
"$lib/*": [
"src/lib/*"
],
"@api": [
"src/api"
]
}
},
}
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": ["es2020", "DOM"],
"moduleResolution": "node",
"module": "es2020",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "es2020",
"importsNotUsedAsValues": "preserve",
"preserveValueImports": false,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"],
"@api": ["src/api"]
}
}
}