user login page and storage

This commit is contained in:
Nikita Galaiko 2023-02-13 13:38:57 +01:00
parent 51bae9bf36
commit 7d86eb365e
No known key found for this signature in database
GPG Key ID: EBAB54E845BA519D
10 changed files with 170 additions and 55 deletions

19
src-tauri/Cargo.lock generated
View File

@ -2110,6 +2110,16 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "open"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8"
dependencies = [
"pathdiff",
"windows-sys 0.42.0",
]
[[package]]
name = "openssl"
version = "0.10.45"
@ -2215,6 +2225,12 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "percent-encoding"
version = "2.2.0"
@ -3327,9 +3343,11 @@ dependencies = [
"ignore",
"objc",
"once_cell",
"open",
"percent-encoding",
"rand 0.8.5",
"raw-window-handle",
"regex",
"rfd",
"semver 1.0.16",
"serde",
@ -3382,6 +3400,7 @@ dependencies = [
"png",
"proc-macro2",
"quote",
"regex",
"semver 1.0.16",
"serde",
"serde_json",

View File

@ -15,7 +15,7 @@ tauri-build = { version = "1.2", features = [] }
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["dialog-open", "fs-create-dir", "fs-exists", "fs-read-file", "fs-write-file", "path-all", "system-tray", "window-start-dragging"] }
tauri = { version = "1.2", features = ["dialog-open", "fs-create-dir", "fs-exists", "fs-read-file", "fs-write-file", "path-all", "shell-open", "system-tray", "window-start-dragging"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev", features = ["colored"] }
log = "0.4.17"

View File

@ -13,6 +13,10 @@
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
},
"dialog": {
"all": false,
"open": true
@ -30,7 +34,9 @@
"writeFile": true,
"exists": true,
"createDir": true,
"scope": ["$APPLOCALDATA/databases", "$APPLOCALDATA/databases/*.json"]
"scope": [
"$APPLOCALDATA/user.json"
]
}
},
"bundle": {

59
src/lib/authentication.ts Normal file
View File

@ -0,0 +1,59 @@
import { dev } from '$app/environment'
const apiUrl = dev ? new URL('https://test.app.gitbutler.com/api/') : new URL('https://app.gitbutler.com/api/');
const getUrl = (path: string) => new URL(path, apiUrl).toString();
export type LoginToken = {
token: string,
expires: string,
url: string
}
export type User = {
id: number,
name: string,
email: string,
picture: string,
locale: string,
created_at: string,
updated_at: string,
access_token: string,
}
const parseJSON = async (response: Response) => {
if (response.status === 204 || response.status === 205) {
return null;
}
if (response.status >= 400) {
throw new Error(`HTTP Error ${response.statusText}: ${await response.text()}`);
}
return await response.json();
}
export default ({ fetch }: { fetch: typeof window.fetch } = { fetch: window.fetch }) => ({
login: {
token: {
create: (params: {} = {}): Promise<LoginToken> => fetch(getUrl('login/token.json'), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params),
}).then(parseJSON).then(token => {
const url = new URL(token.url);
url.host = apiUrl.host;
return {
...token,
url: url.toString(),
}
}),
},
user: {
get: (token: string): Promise<User> => fetch(getUrl(`login/user/${token}.json`), {
method: 'GET',
}).then(parseJSON),
}
}
})

View File

@ -1,34 +0,0 @@
import {
exists,
readTextFile,
BaseDirectory,
writeTextFile,
createDir,
} from "@tauri-apps/api/fs";
import { join } from "@tauri-apps/api/path";
const options = {
dir: BaseDirectory.AppLocalData,
};
export const json = async <T extends any>(filename: string) => {
await createDir("databases", { ...options, recursive: true });
const path = await join("databases", filename);
return {
read: async (): Promise<T | undefined> => {
const isExists = await exists(path, {
dir: BaseDirectory.AppLocalData,
});
if (!isExists) {
return undefined;
} else {
const contents = await readTextFile(path, options);
return JSON.parse(contents) as T;
}
},
write: async (value: T): Promise<void> => {
await writeTextFile(path, JSON.stringify(value), options);
},
};
};

View File

@ -1,5 +1,4 @@
export * as deltas from "./deltas";
export * as database from "./database";
export * as projects from "./projects";
export * as log from "./log";
export * as sessions from "./sessions";

32
src/lib/users.ts Normal file
View File

@ -0,0 +1,32 @@
import { BaseDirectory, exists, readTextFile, writeTextFile } from '@tauri-apps/api/fs';
import type { User } from '$lib/authentication';
import { writable } from 'svelte/store';
const userFile = 'user.json';
const isLoggedIn = () => exists(userFile, {
dir: BaseDirectory.AppLocalData
})
export default async () => {
const store = writable<User | undefined>(undefined);
if (await isLoggedIn()) {
const user = JSON.parse(await readTextFile(userFile, {
dir: BaseDirectory.AppLocalData
})) as User;
store.set(user);
}
store.subscribe(async (user) => {
if (user) {
console.log({ user });
await writeTextFile(userFile, JSON.stringify(user), {
dir: BaseDirectory.AppLocalData
});
}
})
return store;
}

View File

@ -64,7 +64,7 @@
>
<div class="center">Search GitButler</div>
</div>
<div class="mr-4 cursor-default font-bold">User</div>
<a href="/users/" class="mr-4 cursor-default font-bold">User</a>
</header>
<div class="flex h-screen flex-grow flex-row text-zinc-400 overflow-hidden">

View File

@ -1,4 +1,4 @@
import { readable } from "svelte/store";
import { readable, writable } from "svelte/store";
import type { LayoutLoad } from "./$types";
import { building } from "$app/environment";
import type { Project } from "$lib/projects";
@ -8,20 +8,12 @@ export const prerender = true;
export const csr = true;
export const load: LayoutLoad = async () => {
// tauri apis require window reference which doesn't exist during ssr, so we do not import it here.
if (building) {
return {
projects: {
...readable<Project[]>([]),
add: () => {
throw new Error("not implemented");
},
},
};
} else {
const Projects = await import("$lib/projects");
return {
projects: await Projects.default(),
};
}
const projects = building ? ({
...readable<Project[]>([]),
add: () => {
throw new Error("not implemented");
}
}) : await (await import("$lib/projects")).default();
const user = building ? writable<undefined>(undefined) : await (await import("$lib/users")).default();
return { projects, user }
};

View File

@ -0,0 +1,42 @@
<script lang="ts">
import {open} from '@tauri-apps/api/shell'
import Authentication, { type LoginToken } from "$lib/authentication";
import type { PageData } from "./$types";
export let data: PageData;
const { user } = data;
const authApi = Authentication();
const pollForUser = async (token: LoginToken) => {
await open (token.url);
const apiUser = await authApi.login.user.get(token.token).catch(() => null);
if (apiUser) {
user.set(apiUser);
return apiUser;
}
return new Promise((resolve) => {
setTimeout(async () => {
resolve(await pollForUser(token));
}, 1000);
});
}
</script>
{#if $user}
<div>
Welcome, {$user.name}!
</div>
{:else}
{#await authApi.login.token.create()}
<div>loading...</div>
{:then token}
{#await pollForUser(token)}
<div>Log in in your system browser</div>
<p>If you are not redirected automatically, you can
<button on:click={() => pollForUser(token)}>Try again</button>
</p>
{/await}
{/await}
{/if}