Work in progress refactor
This commit is contained in:
parent
efc2b41f9e
commit
879f7aad3f
17
src/app.d.ts
vendored
17
src/app.d.ts
vendored
@ -1,12 +1,23 @@
|
|||||||
|
import type { Session } from 'svelte-kit-cookie-session';
|
||||||
|
|
||||||
|
type SessionData = {
|
||||||
|
views: number;
|
||||||
|
};
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
interface Locals {
|
||||||
// interface PageData {}
|
session: Session<SessionData>;
|
||||||
|
}
|
||||||
|
interface PageData {
|
||||||
|
// can add any properties here, return it from your root layout
|
||||||
|
session: SessionData;
|
||||||
|
}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export { };
|
5
src/hooks.server.ts
Normal file
5
src/hooks.server.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { handleSession } from 'svelte-kit-cookie-session';
|
||||||
|
|
||||||
|
export const handle = handleSession({
|
||||||
|
secret: 'SOME_COMPLEX_SECRET_32_CHARSLONG'
|
||||||
|
});
|
@ -8,55 +8,18 @@
|
|||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import { createLitSession } from "./createLitSession";
|
import { createLitSession } from "./createLitSession";
|
||||||
import { connectProvider } from "./setupLit";
|
import { connectProvider } from "./setupLit";
|
||||||
|
import { googleSession } from "./stores";
|
||||||
|
|
||||||
const redirectUri = "http://localhost:3000/";
|
const redirectUri = "http://localhost:3000/";
|
||||||
|
|
||||||
let authMethod, provider;
|
let authMethod, provider;
|
||||||
let status = "Initializing...";
|
let status = "Initializing...";
|
||||||
let pkps: IRelayPKP[] = [];
|
let pkps: IRelayPKP[] = [];
|
||||||
let view = "SIGN_IN";
|
|
||||||
let myPKP;
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
initialize();
|
initialize();
|
||||||
myPKP = JSON.parse(localStorage.getItem("myPKP"));
|
|
||||||
if (myPKP.active) {
|
|
||||||
view = "READY";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$: {
|
|
||||||
if (myPKP) {
|
|
||||||
if (myPKP.sessionSigs) {
|
|
||||||
myPKP.parsedSigs = parseSessionSigs(myPKP.sessionSigs);
|
|
||||||
console.log("test: " + JSON.stringify(myPKP.parsedSigs));
|
|
||||||
myPKP.active = myPKP.parsedSigs.some((sig) => !sig.isExpired);
|
|
||||||
if (!myPKP.active) {
|
|
||||||
view = "SIGN_IN";
|
|
||||||
} else if (myPKP.active) {
|
|
||||||
view = "READY";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
myPKP.active = false;
|
|
||||||
view = "SIGN_IN";
|
|
||||||
}
|
|
||||||
localStorage.setItem("myPKP", JSON.stringify(myPKP));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function parseSessionSigs(jsonData) {
|
|
||||||
let sessionList = Object.values(jsonData).map((session) => {
|
|
||||||
let sessionData = JSON.parse(session.signedMessage);
|
|
||||||
let expirationDate = new Date(sessionData.expiration);
|
|
||||||
let isExpired = expirationDate < new Date();
|
|
||||||
return {
|
|
||||||
sig: session.sig,
|
|
||||||
expiration: expirationDate,
|
|
||||||
isExpired: isExpired,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return sessionList;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
status = "Connecting to Google provider...";
|
status = "Connecting to Google provider...";
|
||||||
try {
|
try {
|
||||||
@ -75,7 +38,6 @@
|
|||||||
|
|
||||||
async function authWithGoogle() {
|
async function authWithGoogle() {
|
||||||
try {
|
try {
|
||||||
view = "";
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
provider = await connectProvider();
|
provider = await connectProvider();
|
||||||
status = "Reconnected to Google provider.";
|
status = "Reconnected to Google provider.";
|
||||||
@ -89,7 +51,6 @@
|
|||||||
|
|
||||||
async function handleRedirect(providerName: string) {
|
async function handleRedirect(providerName: string) {
|
||||||
try {
|
try {
|
||||||
view = "";
|
|
||||||
if (!provider) throw new Error("Invalid provider.");
|
if (!provider) throw new Error("Invalid provider.");
|
||||||
authMethod = await provider.authenticate();
|
authMethod = await provider.authenticate();
|
||||||
status = "Authenticated successfully.";
|
status = "Authenticated successfully.";
|
||||||
@ -108,7 +69,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function mint() {
|
async function mint() {
|
||||||
view = "";
|
|
||||||
const newPKP: IRelayPKP = await mintPkp(provider, authMethod);
|
const newPKP: IRelayPKP = await mintPkp(provider, authMethod);
|
||||||
pkps = [...pkps, newPKP];
|
pkps = [...pkps, newPKP];
|
||||||
status = "New PKP minted.";
|
status = "New PKP minted.";
|
||||||
@ -117,7 +77,6 @@
|
|||||||
|
|
||||||
async function createSession(pkp: IRelayPKP) {
|
async function createSession(pkp: IRelayPKP) {
|
||||||
try {
|
try {
|
||||||
view = "";
|
|
||||||
const currentPKP = pkp; // Assign the selected PKP to currentPKP
|
const currentPKP = pkp; // Assign the selected PKP to currentPKP
|
||||||
const sessionSigs = await createLitSession(
|
const sessionSigs = await createLitSession(
|
||||||
provider,
|
provider,
|
||||||
@ -131,70 +90,29 @@
|
|||||||
provider: "google",
|
provider: "google",
|
||||||
pkp: currentPKP,
|
pkp: currentPKP,
|
||||||
sessionSigs: sessionSigs,
|
sessionSigs: sessionSigs,
|
||||||
active: true,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
myPKP = JSON.parse(localStorage.getItem("myPKP"));
|
|
||||||
status = "Session created successfully.";
|
status = "Session created successfully.";
|
||||||
view = "READY";
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function clearSession() {
|
|
||||||
localStorage.removeItem("myPKP");
|
|
||||||
myPKP = null;
|
|
||||||
view = "SIGN_IN";
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if view === "SIGN_IN"}
|
<div class="p-8 bg-white bg-opacity-75 rounded shadow-md">
|
||||||
<div class="p-8 bg-white bg-opacity-75 rounded shadow-md">
|
<button
|
||||||
<button
|
on:click={authWithGoogle}
|
||||||
on:click={authWithGoogle}
|
class="w-full py-2 text-white bg-blue-500 rounded hover:bg-blue-700 flex items-center justify-center"
|
||||||
class="w-full py-2 text-white bg-blue-500 rounded hover:bg-blue-700 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<span class="mr-2"><Icon icon="flat-color-icons:google" /></span>
|
|
||||||
<span>Sign in with Google</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if view != "READY"}
|
|
||||||
<div class="px-4 bg-white bg-opacity-75 rounded shadow-md">
|
|
||||||
<div class="mt-4 text-center">
|
|
||||||
<p class="text-gray-600">{status}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if view === "READY"}
|
|
||||||
<div
|
|
||||||
class="fixed bottom-0 left-0 right-0 p-3 bg-white bg-opacity-75 rounded-t-lg shadow-md flex flex-col items-center space-y-4"
|
|
||||||
>
|
>
|
||||||
<div class="w-full flex items-center justify-between space-x-4">
|
<span class="mr-2"><Icon icon="flat-color-icons:google" /></span>
|
||||||
<div class="flex items-center space-x-2">
|
<span>Sign in with Google</span>
|
||||||
<Icon
|
</button>
|
||||||
icon="ic:baseline-account-circle"
|
</div>
|
||||||
class="text-gray-500 w-12 h-12"
|
<div class="px-4 bg-white bg-opacity-75 rounded shadow-md">
|
||||||
/>
|
<div class="mt-4 text-center">
|
||||||
<div>
|
<p class="text-gray-600">{status}</p>
|
||||||
<p class="text-sm">
|
|
||||||
<span class="font-semibold">Address:</span>
|
|
||||||
{myPKP.pkp.ethAddress}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs">
|
|
||||||
<span class="font-semibold">Provider:</span>
|
|
||||||
{myPKP.provider}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
on:click={clearSession}
|
|
||||||
class="py-1 px-2 text-white bg-red-500 rounded hover:bg-red-700"
|
|
||||||
>
|
|
||||||
Clear Session
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
85
src/lib/GoogleSigner.svelte
Normal file
85
src/lib/GoogleSigner.svelte
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import Icon from "@iconify/svelte";
|
||||||
|
import { googleSession } from "$lib/stores.js";
|
||||||
|
|
||||||
|
let myPKP;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
myPKP = JSON.parse(localStorage.getItem("myPKP"));
|
||||||
|
if (myPKP) {
|
||||||
|
let parsedSigs = parseSessionSigs(myPKP.sessionSigs);
|
||||||
|
let active = parsedSigs.some((sig) => !sig.isExpired);
|
||||||
|
if (!active) {
|
||||||
|
clearSession();
|
||||||
|
googleSession.set({ activeSession: false });
|
||||||
|
} else {
|
||||||
|
googleSession.set({ activeSession: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (myPKP) {
|
||||||
|
let parsedSigs = parseSessionSigs(myPKP.sessionSigs);
|
||||||
|
let active = parsedSigs.some((sig) => !sig.isExpired);
|
||||||
|
googleSession.set({ activeSession: true });
|
||||||
|
if (!active) {
|
||||||
|
clearSession();
|
||||||
|
googleSession.set({ activeSession: false });
|
||||||
|
} else {
|
||||||
|
googleSession.set({ activeSession: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSessionSigs(jsonData) {
|
||||||
|
let sessionList = Object.values(jsonData).map((session) => {
|
||||||
|
let sessionData = JSON.parse(session.signedMessage);
|
||||||
|
let expirationDate = new Date(sessionData.expiration);
|
||||||
|
let isExpired = expirationDate < new Date();
|
||||||
|
return {
|
||||||
|
sig: session.sig,
|
||||||
|
expiration: expirationDate,
|
||||||
|
isExpired: isExpired,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return sessionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSession() {
|
||||||
|
localStorage.removeItem("myPKP");
|
||||||
|
myPKP = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if myPKP}
|
||||||
|
<div
|
||||||
|
class="fixed bottom-0 left-0 right-0 p-3 bg-white bg-opacity-75 rounded-t-lg shadow-md flex flex-col items-center space-y-4"
|
||||||
|
>
|
||||||
|
<div class="w-full flex items-center justify-between space-x-4">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Icon
|
||||||
|
icon="ic:baseline-account-circle"
|
||||||
|
class="text-gray-500 w-12 h-12"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm">
|
||||||
|
<span class="font-semibold">Address:</span>
|
||||||
|
{myPKP.pkp.ethAddress}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs">
|
||||||
|
<span class="font-semibold">Provider:</span>
|
||||||
|
{myPKP.provider}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
on:click={clearSession}
|
||||||
|
class="py-1 px-2 text-white bg-red-500 rounded hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Clear Session
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
103
src/lib/Test.svelte
Normal file
103
src/lib/Test.svelte
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script>
|
||||||
|
let litNodeClient;
|
||||||
|
|
||||||
|
litNodeClient.connect();
|
||||||
|
|
||||||
|
var authSig = JSON.parse(
|
||||||
|
'{"sig":"0x18a173d68d2f78cc5c13da0dfe36eec2a293285bee6d42547b9577bf26cdc985660ed3dddc4e75d422366cac07e8a9fc77669b10373bef9c7b8e4280252dfddf1b","derivedVia":"web3.eth.personal.sign","signedMessage":"I am creating an account to use LITs at 2021-08-04T20:14:04.918Z","address":"0xdbd360f30097fb6d938dcc8b7b62854b36160b45"}'
|
||||||
|
);
|
||||||
|
|
||||||
|
var randomPath = () =>
|
||||||
|
"/" +
|
||||||
|
Math.random().toString(36).substring(2, 15) +
|
||||||
|
Math.random().toString(36).substring(2, 15);
|
||||||
|
var testProvisoningAndSigning = async ({
|
||||||
|
unifiedAccessControlConditions,
|
||||||
|
testName,
|
||||||
|
}) => {
|
||||||
|
document.getElementById("status").innerText = `Testing ${testName}...`;
|
||||||
|
document.getElementById(
|
||||||
|
"humanized"
|
||||||
|
).innerText = `Humanized: ${await litNodeClient.humanizeAccessControlConditions(
|
||||||
|
{
|
||||||
|
unifiedAccessControlConditions,
|
||||||
|
}
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
var solAuthSig = await litNodeClient.checkAndSignAuthMessage({
|
||||||
|
chain: "solana",
|
||||||
|
});
|
||||||
|
var ethAuthSig = await litNodeClient.checkAndSignAuthMessage({
|
||||||
|
chain: "ethereum",
|
||||||
|
});
|
||||||
|
|
||||||
|
let resourceId = {
|
||||||
|
baseUrl: "my-dynamic-content-server.com",
|
||||||
|
path: randomPath(),
|
||||||
|
orgId: "",
|
||||||
|
role: "",
|
||||||
|
extraData: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await litNodeClient.saveSigningCondition({
|
||||||
|
unifiedAccessControlConditions,
|
||||||
|
authSig: {
|
||||||
|
solana: solAuthSig,
|
||||||
|
ethereum: ethAuthSig,
|
||||||
|
},
|
||||||
|
resourceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
let jwt = await litNodeClient.getSignedToken({
|
||||||
|
unifiedAccessControlConditions,
|
||||||
|
authSig: {
|
||||||
|
solana: solAuthSig,
|
||||||
|
ethereum: ethAuthSig,
|
||||||
|
},
|
||||||
|
resourceId,
|
||||||
|
});
|
||||||
|
console.log("jwt", jwt);
|
||||||
|
|
||||||
|
// uncomment this to break the jwt, to test an invalid jwt
|
||||||
|
// jwt = jwt.replace(/.$/, "3");
|
||||||
|
|
||||||
|
const { verified, header, payload } = litNodeClient.verifyJwt({ jwt });
|
||||||
|
console.log("verified", verified);
|
||||||
|
console.log("header", header);
|
||||||
|
console.log("payload", payload);
|
||||||
|
|
||||||
|
if (jwt && verified) {
|
||||||
|
document.getElementById("status").innerText = `${testName}: Success`;
|
||||||
|
} else {
|
||||||
|
document.getElementById("status").innerText = `${testName}: Failure`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var IsPermittedAction = async () => {
|
||||||
|
/*
|
||||||
|
{ contract_address: "0x9e1DDB2499C6834204347F047Ace1ae18E830449", chain: "mumbai", standard_contract_type: "PubkeyRouterAndPermissions", method: "isPermittedAction", parameters: ["0xab9704fbd33d96c0475f6d2f1e6e7ff3497d4eceb10df78d0fcf012ab3b09300", "0x12203577a857f9d58507be2f4a87d969cc582dd00a1d0486281113e68208163cb5e8"], return_value_test: JsonReturnValueTest { comparator: "=", value: "true" } }
|
||||||
|
*/
|
||||||
|
var unifiedAccessControlConditions = [
|
||||||
|
{
|
||||||
|
conditionType: "evmBasic",
|
||||||
|
contractAddress: "0x9e1DDB2499C6834204347F047Ace1ae18E830449",
|
||||||
|
chain: "mumbai",
|
||||||
|
standardContractType: "PubkeyRouterAndPermissions",
|
||||||
|
method: "isPermittedAction",
|
||||||
|
parameters: [
|
||||||
|
"0xab9704fbd33d96c0475f6d2f1e6e7ff3497d4eceb10df78d0fcf012ab3b09300",
|
||||||
|
"0x12203577a857f9d58507be2f4a87d969cc582dd00a1d0486281113e68208163cb5e8",
|
||||||
|
],
|
||||||
|
returnValueTest: { comparator: "=", value: "true" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await testProvisoningEncryptingAndDecrypting({
|
||||||
|
unifiedAccessControlConditions,
|
||||||
|
testName: "IsPermittedAction",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<button on:click={IsPermittedAction}>IsPermittedAction</button>
|
||||||
|
<br />
|
@ -2,4 +2,8 @@ import { writable } from 'svelte/store';
|
|||||||
|
|
||||||
export const signRequest = writable({json: {}});
|
export const signRequest = writable({json: {}});
|
||||||
|
|
||||||
export const signedMessages = writable([])
|
export const signedMessages = writable([])
|
||||||
|
|
||||||
|
export const googleSession = writable({
|
||||||
|
activeSession: false
|
||||||
|
});
|
@ -6,11 +6,20 @@
|
|||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { initChainProvider } from "$lib/setupChainProvider";
|
import { initChainProvider } from "$lib/setupChainProvider";
|
||||||
|
import { googleSession } from "$lib/stores.js";
|
||||||
|
import GoogleSigner from "$lib/GoogleSigner.svelte";
|
||||||
|
import GooglePKP from "$lib/GooglePKP.svelte";
|
||||||
|
|
||||||
|
let activeSession = true;
|
||||||
|
|
||||||
export let data: LayoutData;
|
export let data: LayoutData;
|
||||||
|
|
||||||
const token = Cookies.get("token");
|
const token = Cookies.get("token");
|
||||||
|
|
||||||
|
googleSession.subscribe((value) => {
|
||||||
|
activeSession = value.activeSession;
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
initChainProvider();
|
initChainProvider();
|
||||||
});
|
});
|
||||||
@ -25,6 +34,9 @@
|
|||||||
style="background-image: url('lake.jpeg');"
|
style="background-image: url('lake.jpeg');"
|
||||||
>
|
>
|
||||||
<QueryClientProvider client={data.queryClient}>
|
<QueryClientProvider client={data.queryClient}>
|
||||||
|
<div class="text-lg bg-white">{activeSession}</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
{#if activeSession}active {:else} <GooglePKP /> {/if}
|
||||||
|
<GoogleSigner />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,5 @@ export const load: LayoutLoad = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { queryClient };
|
return { queryClient };
|
||||||
};
|
};
|
||||||
|
10
src/routes/+page.server.ts
Normal file
10
src/routes/+page.server.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { ServerLoad } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: ServerLoad = async ({ locals }) => {
|
||||||
|
await locals.session.set({ myPKP: "hello1" });
|
||||||
|
|
||||||
|
return {
|
||||||
|
myPKP: locals.session.data.myPKP
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,23 +1,14 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import GooglePKP from "$lib/GooglePKP.svelte";
|
// import { signRequest } from "$lib/stores.js";
|
||||||
// import { signRequest, signedMessages } from "$lib/stores.js";
|
|
||||||
|
|
||||||
// function trigger() {
|
// function trigger() {
|
||||||
// signRequest.set({ json: { hello: "test" } });
|
// signRequest.set({ json: { hello: "test" } });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// import type { PageData } from "./$types";
|
||||||
|
// export let data: PageData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<GooglePKP />
|
<!-- <div class="bg-white">myPKP {data.myPKP}</div> -->
|
||||||
<!-- <button on:click={trigger}>Sign Request</button> -->
|
|
||||||
|
|
||||||
<!-- <ul>
|
<!-- <button on:click={trigger}>Sign Request</button> -->
|
||||||
{#each $signedMessages as { json, signature }}
|
|
||||||
<li>
|
|
||||||
<strong>Message:</strong>
|
|
||||||
{JSON.stringify(json)}
|
|
||||||
<br />
|
|
||||||
<strong>Signature:</strong>
|
|
||||||
{JSON.stringify(signature)}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul> -->
|
|
||||||
|
Loading…
Reference in New Issue
Block a user