login
Seto Elkahfi committed
Jul 31, 2024 at 16:35 UTC
31c36a072ef426d03f210abf0acd5871aca44425
16 files changed
+327
-48
backend/musik88-web/app/models/user.rb
+1
-1
@@ -167,7 +167,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
167
end
168
169
def as_json_packed
170
- as_json(only: %i[id username name about])
170
+ as_json(only: %i[id email username name about])
171
end
172
173
def followers_count
frontend/splitfire-desktop/app/_src/models/user.tsx
+2
-1
@@ -6,7 +6,8 @@ export default interface User {
6
gravatar_url: string,
7
followers_count: number,
8
following_count: number,
9
- about: string
9
+ about: string,
10
+ access_token: string | null,
11
}
12
13
export function usernameOrId(user: User): string {
frontend/splitfire-desktop/app/_ui/global-nav.tsx
+23
-6
@@ -5,12 +5,14 @@ import Link from 'next/link';
5
import { useSelectedLayoutSegment } from 'next/navigation';
6
import { MenuAlt2Icon, XIcon } from '@heroicons/react/solid';
7
import clsx from 'clsx';
8
-import { useState } from 'react';
8
+import { useContext, useState } from 'react';
9
import Image from 'next/image';
10
+import { UserContext } from '../_src/lib/CurrentUserContext';
11
12
export function GlobalNav() {
13
const [isOpen, setIsOpen] = useState(false);
14
const close = () => setIsOpen(false);
15
+ const user = useContext(UserContext);
16
17
return (
18
<div className="fixed top-0 z-10 flex w-full flex-col border-b border-gray-800 bg-black lg:bottom-0 lg:z-auto lg:w-72 lg:border-b-0 lg:border-r lg:border-gray-800">
@@ -67,17 +69,32 @@ export function GlobalNav() {
69
<div className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400/80">
70
<div>{"Account"}</div>
71
</div>
70
-
71
- <div className="space-y-1">
72
- <GlobalNavItem key={"login"} item={{name: "Login", slug: "login"}} close={close} />
73
- <GlobalNavItem key={"register"} item={{name: "Register", slug: "register"}} close={close} />
74
- </div>
72
+
73
+ {user.user ? <LoggedInNav /> : <LoggedOutNav />}
74
</nav>
75
</div>
76
</div>
77
);
78
}
79
80
+function LoggedOutNav() {
81
+ return (
82
+ <div className="space-y-1">
83
+ <GlobalNavItem item={{ name: 'Login', slug: 'login' }} close={() => {}} />
84
+ <GlobalNavItem item={{ name: 'Register', slug: 'register' }} close={() => {}} />
85
+ </div>
86
+ );
87
+}
88
+
89
+function LoggedInNav() {
90
+ return (
91
+ <div className="space-y-1">
92
+ <GlobalNavItem item={{ name: 'Profile', slug: 'profile' }} close={() => {}} />
93
+ <GlobalNavItem item={{ name: 'Logout', slug: 'logout' }} close={() => {}} />
94
+ </div>
95
+ );
96
+}
97
+
98
function GlobalNavItem({
99
item,
100
close,
frontend/splitfire-desktop/app/layout.tsx
+69
-16
@@ -1,36 +1,89 @@
1
-import { Metadata } from "next";
1
+ "use client"
2
+
3
import { AddressBar } from "./_ui/address-bar";
4
import { GlobalNav } from "./_ui/global-nav";
5
import "./globals.css";
6
+import { useEffect, useState } from "react";
7
+import { CurrentUser, CurrentUserType, db } from "./_src/lib/db";
8
+import { UserContext } from "./_src/lib/CurrentUserContext";
9
+import { useLogger } from "./_src/lib/logger";
10
6
-export const metadata: Metadata = {
7
- title: "SplitFire",
8
- description: "an intelligent exression engine.",
9
-};
11
+enum State {
12
+ LOADING,
13
+ LOADED,
14
+ ERROR
15
+}
16
17
export default function RootLayout({
18
children,
19
}: {
20
children: React.ReactNode;
21
}) {
22
+
23
+ const log = useLogger('App')
24
+ const [state, setState] = useState(State.LOADING)
25
+ const [user, setUser] = useState<CurrentUser | null>(null)
26
+
27
+ const updateUser = (newUser: CurrentUser) => {
28
+ setUser(newUser)
29
+ }
30
+
31
+ const getCurrentUser = async () => {
32
+ setState(State.LOADING)
33
+ try {
34
+ const result = await db.currentUser.where({ type: CurrentUserType.MAIN }).first()
35
+ log.debug('Result: ', result)
36
+ if (result) {
37
+ setUser(result)
38
+ }
39
+ setState(State.LOADED)
40
+ } catch (error) {
41
+ log.error(error)
42
+ setState(State.ERROR)
43
+ }
44
+ }
45
+
46
+ useEffect(() => {
47
+ getCurrentUser()
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, [])
50
+
51
+ if (state === State.LOADING) {
52
+ return ( <html lang="en" className="[color-scheme:dark]">
53
+ <body className="bg-gray-1100 overflow-y-scroll bg-[url('/grid.svg')] pb-36">
54
+ Loading app ...
55
+ </body>
56
+ </html>);
57
+ }
58
+
59
+ if (state === State.ERROR) {
60
+ return ( <html lang="en" className="[color-scheme:dark]">
61
+ <body className="bg-gray-1100 overflow-y-scroll bg-[url('/grid.svg')] pb-36">
62
+ Error loading app ...
63
+ </body>
64
+ </html>);
65
+ }
66
+
67
return (
68
<html lang="en" className="[color-scheme:dark]">
69
<body className="bg-gray-1100 overflow-y-scroll bg-[url('/grid.svg')] pb-36">
19
- <GlobalNav />
20
- <div className="lg:pl-72">
21
- <div className="bg-vc-border-gradient rounded-lg p-px shadow-lg shadow-black/20">
22
- <div className="rounded-lg bg-black">
23
- <AddressBar />
24
- </div>
25
- </div>
26
- <div className="mx-auto max-w-4xl space-y-8 px-2 pt-20 lg:px-8 lg:py-8">
70
+ <UserContext.Provider value={{ user, updateUser }}>
71
+ <GlobalNav />
72
+ <div className="lg:pl-72">
73
<div className="bg-vc-border-gradient rounded-lg p-px shadow-lg shadow-black/20">
28
- <div className="rounded-lg bg-black p-3.5 lg:p-6">
29
- {children}
74
+ <div className="rounded-lg bg-black">
75
+ <AddressBar />
76
+ </div>
77
+ </div>
78
+ <div className="mx-auto max-w-4xl space-y-8 px-2 pt-20 lg:px-8 lg:py-8">
79
+ <div className="bg-vc-border-gradient rounded-lg p-px shadow-lg shadow-black/20">
80
+ <div className="rounded-lg bg-black p-3.5 lg:p-6">
81
+ {children}
82
+ </div>
83
</div>
84
</div>
85
</div>
33
- </div>
86
+ </UserContext.Provider>
87
</body>
88
</html>
89
);
frontend/splitfire-desktop/app/login/page.tsx
+32
-3
@@ -2,19 +2,30 @@
2
3
import * as Form from "@radix-ui/react-form";
4
import { invoke } from "@tauri-apps/api/tauri";
5
-import { TAURI_ACCOUNT_LOGIN } from "../_src/lib/tauriHandler";
5
+import { TAURI_ACCOUNT_LOGIN, TauriResponse } from "../_src/lib/tauriHandler";
6
import { Button } from "../_ui/components/button";
7
import { useState } from "react";
8
import { UserContext } from "../_src/lib/CurrentUserContext";
9
import { CurrentUser, CurrentUserType, db } from "../_src/lib/db";
10
import { AccountLoginResponse } from "@/models/account";
11
import { Mode } from "../_src/components/player/models/Mode";
12
+import { LoadingView } from "../_src/components/templates/LoadingView";
13
+import { useRouter } from "next/navigation";
14
+
15
+enum State {
16
+ LOADING,
17
+ LOADED,
18
+}
19
20
export default function Page() {
21
+
22
+ const router = useRouter();
23
+ const [state, setState] = useState(State.LOADED);
24
const [emailOrUsername, setEmailOrUsername] = useState("");
25
const [password, setPassword] = useState("");
26
27
const login = async (update: (user: CurrentUser) => void) => {
28
+ setState(State.LOADING);
29
const payload = {
30
username: emailOrUsername,
31
password: password,
@@ -22,16 +33,34 @@ export default function Page() {
33
console.log("payload", payload);
34
const response: AccountLoginResponse = await invoke(TAURI_ACCOUNT_LOGIN, payload);
35
console.log("response", response);
36
+ if (response.status === TauriResponse.ERROR) {
37
+ console.error("Login failed", response);
38
+ return;
39
+ }
40
+ // Sanity check
41
+ if (response.user === null || response.accessToken === null) {
42
+ console.error("Login failed", response);
43
+ return;
44
+ }
45
+
46
let currentUser: CurrentUser = {
26
- accessToken: "",
47
+ accessToken: response.accessToken,
48
type: CurrentUserType.MAIN,
49
user: response.user,
50
mode: Mode.Vocal
30
- }
51
+ }
52
db.currentUser.add(currentUser)
53
+ // Update UserContext
54
update(currentUser);
55
+
56
+ setState(State.LOADED);
57
+ router.push("/");
58
};
59
60
+ if (state === State.LOADING) {
61
+ return <LoadingView />;
62
+ }
63
+
64
return (
65
<div className="prose prose-sm prose-invert max-w-none">
66
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
frontend/splitfire-desktop/app/logout/layout.tsx
new
+25
@@ -0,0 +1,25 @@
1
+import { Metadata } from 'next';
2
+import React from 'react';
3
+
4
+export const metadata: Metadata = {
5
+ title: "Play",
6
+ description: "an intelligent exression engine.",
7
+};
8
+
9
+
10
+export default async function Layout({
11
+ children,
12
+}: {
13
+ children: React.ReactNode;
14
+}) {
15
+ return (
16
+ <div className="space-y-9">
17
+ <div className="flex justify-between">
18
+ <div className="self-start">
19
+ <h1 className="text-3xl font-bold">Logging you out...</h1>
20
+ </div>
21
+ </div>
22
+ <div>{children}</div>
23
+ </div>
24
+ );
25
+}
frontend/splitfire-desktop/app/logout/page.tsx
new
+20
@@ -0,0 +1,20 @@
1
+export async function generateStaticParams() {
2
+ const posts = [
3
+ { slug: "post-1" },
4
+ { slug: "post-2" },
5
+ { slug: "post-3" },
6
+ ];
7
+
8
+ return posts.map((post) => ({
9
+ slug: post.slug,
10
+ }))
11
+}
12
+
13
+export default function Page() {
14
+ return (
15
+ <div className="prose prose-sm prose-invert max-w-none">
16
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
17
+ </div>
18
+ </div>
19
+ );
20
+}
frontend/splitfire-desktop/app/profile/layout.tsx
new
+25
@@ -0,0 +1,25 @@
1
+import { Metadata } from 'next';
2
+import React from 'react';
3
+
4
+export const metadata: Metadata = {
5
+ title: "Play",
6
+ description: "an intelligent exression engine.",
7
+};
8
+
9
+
10
+export default async function Layout({
11
+ children,
12
+}: {
13
+ children: React.ReactNode;
14
+}) {
15
+ return (
16
+ <div className="space-y-9">
17
+ <div className="flex justify-between">
18
+ <div className="self-start">
19
+ <h1 className="text-3xl font-bold">Profile</h1>
20
+ </div>
21
+ </div>
22
+ <div>{children}</div>
23
+ </div>
24
+ );
25
+}
frontend/splitfire-desktop/app/profile/page.tsx
new
+29
@@ -0,0 +1,29 @@
1
+import { SkeletonCard } from '../_ui/skeleton-card';
2
+
3
+export async function generateStaticParams() {
4
+ const posts = [
5
+ { slug: "post-1" },
6
+ { slug: "post-2" },
7
+ { slug: "post-3" },
8
+ ];
9
+
10
+ return posts.map((post) => ({
11
+ slug: post.slug,
12
+ }))
13
+}
14
+
15
+export default function Page() {
16
+ return (
17
+ <div className="prose prose-sm prose-invert max-w-none">
18
+ <div className="max-w-none">
19
+ Your profile
20
+ </div>
21
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
22
+ <h2 className="text-2xl font-bold">Your plays</h2>
23
+ {Array.from({ length: 6 }).map((_, i) => (
24
+ <SkeletonCard key={i} />
25
+ ))}
26
+ </div>
27
+ </div>
28
+ );
29
+}
frontend/splitfire-desktop/models/account.ts
+2
-1
@@ -4,5 +4,6 @@ import User from "@/app/_src/models/user";
4
export interface AccountLoginResponse {
5
status: TauriResponse,
6
message: string,
7
- user: User
7
+ accessToken: string | null,
8
+ user: User | null
9
}
frontend/splitfire-desktop/src-tauri/Cargo.lock
+14
-1
@@ -114,6 +114,7 @@ dependencies = [
114
"tauri",
115
"tauri-build",
116
"tauri-plugin-log",
117
+ "tauri-plugin-store",
118
"url-builder",
119
]
120
@@ -3707,7 +3708,7 @@ dependencies = [
3708
[[package]]
3709
name = "tauri-plugin-log"
3710
version = "0.0.0"
3710
-source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#6a6c9daeb261a29707c9d011346c8b7746ae6f54"
3711
+source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#1da7892632044a27280733e4297cab135cffe316"
3712
dependencies = [
3713
"byte-unit",
3714
"fern",
@@ -3719,6 +3720,18 @@ dependencies = [
3720
"time",
3721
]
3722
3723
+[[package]]
3724
+name = "tauri-plugin-store"
3725
+version = "0.0.0"
3726
+source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#1da7892632044a27280733e4297cab135cffe316"
3727
+dependencies = [
3728
+ "log",
3729
+ "serde",
3730
+ "serde_json",
3731
+ "tauri",
3732
+ "thiserror",
3733
+]
3734
+
3735
[[package]]
3736
name = "tauri-runtime"
3737
version = "0.14.4"
frontend/splitfire-desktop/src-tauri/Cargo.toml
+1
@@ -29,6 +29,7 @@ serde_json = "1"
29
serde_repr = "0.1"
30
tauri = { version = "1.7.1", features = [] }
31
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] }
32
+tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
33
url-builder = "0.1.1"
34
35
[features]
frontend/splitfire-desktop/src-tauri/src/main.rs
+16
@@ -24,6 +24,7 @@ use app::{
24
sfai_home_dir_path,
25
};
26
use tauri_plugin_log::fern::colors::{Color, ColoredLevelConfig};
27
+use tauri_plugin_store::StoreBuilder;
28
29
fn main() {
30
// Create ~/.sfai directory if it doesn't exist
@@ -39,6 +40,21 @@ fn main() {
40
)
41
.build(),
42
)
43
+ .plugin(
44
+ tauri_plugin_store::Builder::default().build(),
45
+ )
46
+ .setup(|app| {
47
+ let mut store = StoreBuilder::new(app.handle(), "splitfire.bin".parse()?).build();
48
+ match store.load() {
49
+ Ok(_) => {
50
+ println!("Store loaded successfully.");
51
+ }
52
+ Err(e) => {
53
+ println!("Failed to load store: {:?}", e);
54
+ }
55
+ }
56
+ Ok(())
57
+ })
58
.invoke_handler(tauri::generate_handler![
59
account_login,
60
account_register,
frontend/splitfire-desktop/src-tauri/src/models/account.rs
+13
-12
@@ -4,27 +4,28 @@ use super::player::TauriResponse;
4
#[derive(Serialize)]
5
#[derive(Debug)]
6
pub struct AccountLoginResponse {
7
- status: TauriResponse,
8
- message: String,
9
- user: Option<User>,
7
+ pub status: TauriResponse,
8
+ pub message: String,
9
+ pub access_token: Option<String>,
10
+ pub user: Option<User>,
11
}
12
13
#[derive(Serialize, Deserialize)]
14
#[derive(Debug)]
15
pub struct LoginResponse {
16
code: i32,
16
- message: String,
17
- user: User,
17
+ pub message: String,
18
+ pub user: Option<User>,
19
}
20
21
#[derive(Serialize, Deserialize, Debug)]
22
pub struct User {
23
id: i32,
23
- email: String,
24
- username: String,
25
- name: String,
26
- gravatar_url: String,
27
- followers_count: i32,
28
- following_count: i32,
29
- about: String
24
+ pub email: String,
25
+ pub username: String,
26
+ pub name: String,
27
+ pub gravatar_url: String,
28
+ pub followers_count: i32,
29
+ pub following_count: i32,
30
+ pub about: String
31
}
\ No newline at end of file
frontend/splitfire-desktop/src-tauri/src/rest/account.rs
+54
-6
@@ -2,11 +2,11 @@ use log::debug;
2
use reqwest::Client;
3
use serde_json::json;
4
use tauri;
5
-use crate::{command::constants::{base_url_builder, PATH_ACCOUNT_LOGIN, PATH_ACCOUNT_REGISTER}, models::account::LoginResponse};
5
+use crate::{command::constants::{base_url_builder, PATH_ACCOUNT_LOGIN, PATH_ACCOUNT_REGISTER}, models::{account::{AccountLoginResponse, LoginResponse}, player::TauriResponse}};
6
7
#[tauri::command]
8
-pub async fn account_login(username: String, password: String) {
9
- debug!("Logging in with username {}.", username);
8
+pub async fn account_login(username: String, password: String) -> AccountLoginResponse {
9
+ debug!("Logging in with username {}, password {}", username, password);
10
// Login
11
let body = json!( {
12
"username": username,
@@ -20,18 +20,66 @@ pub async fn account_login(username: String, password: String) {
20
21
match response {
22
Ok(response) => {
23
- debug!("Got response: {:?}", response);
23
+ let token = match response.headers().get("Authorization") {
24
+ Some(token) => {
25
+ // Trim Bearer prefix
26
+ let token = match token.to_str() {
27
+ Ok(token) => {
28
+ token.trim_start_matches("Bearer ")
29
+ },
30
+ Err(e) => {
31
+ println!("Failed to get token: {:?}", e);
32
+ return AccountLoginResponse {
33
+ status: TauriResponse::Error,
34
+ message: "Failed to get token".to_string(),
35
+ access_token: None,
36
+ user: None
37
+ };
38
+ }
39
+ };
40
+ Some(token.to_string())
41
+ },
42
+ None => {
43
+ println!("Failed to get token");
44
+ return AccountLoginResponse {
45
+ status: TauriResponse::Error,
46
+ message: "Failed to get token".to_string(),
47
+ access_token: None,
48
+ user: None
49
+ };
50
+ }
51
+ };
52
let res: LoginResponse = match response.json().await {
25
- Ok(json) => json,
53
+ Ok(json) => {
54
+ json
55
+ },
56
Err(e) => {
57
println!("Failed to parse response: {:?}", e);
28
- return;
58
+ return AccountLoginResponse {
59
+ status: TauriResponse::Error,
60
+ message: "Failed to parse response".to_string(),
61
+ access_token: None,
62
+ user: None
63
+ };
64
}
65
};
66
+
67
debug!("Login response: {:?}", res);
68
+ AccountLoginResponse {
69
+ status: TauriResponse::Success,
70
+ message: res.message,
71
+ access_token: token,
72
+ user: res.user
73
+ }
74
}
75
Err(e) => {
76
println!("Failed to get response: {:?}", e);
77
+ AccountLoginResponse {
78
+ status: TauriResponse::Error,
79
+ message: "Failed to get response".to_string(),
80
+ access_token: None,
81
+ user: None
82
+ }
83
}
84
}
85
}
frontend/splitfire-desktop/src-tauri/tauri.conf.json
+1
-1
@@ -6,7 +6,7 @@
6
"distDir": "../build"
7
},
8
"package": {
9
- "productName": "SplitFire intelligent expression engine",
9
+ "productName": "SplitFire",
10
"version": "0.3.4"
11
},
12
"tauri": {