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": {