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:
@@ -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) {
|
||||
export const load: Load = async ({ params }) => {
|
||||
try {
|
||||
const albumId = params['albumId'];
|
||||
|
||||
const { data: albumInfo } = await api.albumApi.getAlbumInfo(albumId);
|
||||
|
||||
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: '/auth/login'
|
||||
};
|
||||
}
|
||||
const albumId = params['albumId'];
|
||||
|
||||
let album: AlbumResponseDto;
|
||||
|
||||
try {
|
||||
const { data } = await api.albumApi.getAlbumInfo(albumId);
|
||||
album = data;
|
||||
} catch (e) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/albums'
|
||||
};
|
||||
}
|
||||
|
||||
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'
|
||||
};
|
||||
}
|
||||
|
||||
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 () => {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user