mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
feat: add login page (#93)
* feat: add signIn * feat: add connect service * feat: add login page
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -8,6 +8,7 @@
|
||||
"dyld",
|
||||
"fullscreen",
|
||||
"headlessui",
|
||||
"Icdbb",
|
||||
"icns",
|
||||
"INFINI",
|
||||
"inputbox",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.1.10",
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@tauri-apps/api": ">=2.0.0",
|
||||
"@tauri-apps/plugin-autostart": "~2",
|
||||
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@headlessui/react':
|
||||
specifier: ^2.1.10
|
||||
version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@react-oauth/google':
|
||||
specifier: ^0.12.1
|
||||
version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@tauri-apps/api':
|
||||
specifier: '>=2.0.0'
|
||||
version: 2.0.2
|
||||
@@ -501,6 +504,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@react-oauth/google@0.12.1':
|
||||
resolution: {integrity: sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@react-stately/utils@3.10.4':
|
||||
resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==}
|
||||
peerDependencies:
|
||||
@@ -2602,6 +2611,11 @@ snapshots:
|
||||
clsx: 2.1.1
|
||||
react: 18.3.1
|
||||
|
||||
'@react-oauth/google@0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@react-stately/utils@3.10.4(react@18.3.1)':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.13
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"core:window:allow-get-all-windows",
|
||||
"core:window:allow-set-focus",
|
||||
"core:app:allow-set-app-theme",
|
||||
"shell:allow-open",
|
||||
"shell:default",
|
||||
"http:default",
|
||||
"http:allow-fetch",
|
||||
"http:allow-fetch-cancel",
|
||||
|
||||
BIN
src/assets/images/apple.png
Normal file
BIN
src/assets/images/apple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/images/bg-login.png
Normal file
BIN
src/assets/images/bg-login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/assets/images/coco-logo.png
Normal file
BIN
src/assets/images/coco-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/images/github.png
Normal file
BIN
src/assets/images/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/images/google.png
Normal file
BIN
src/assets/images/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -5,6 +5,8 @@ import {
|
||||
CornerDownLeft,
|
||||
} from "lucide-react";
|
||||
|
||||
import logoImg from "@/assets/32x32.png";
|
||||
|
||||
interface FooterProps {
|
||||
isChat: boolean;
|
||||
name?: string;
|
||||
@@ -17,6 +19,13 @@ export default function Footer({ name }: FooterProps) {
|
||||
className="px-4 z-999 mx-[1px] h-10 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-4">
|
||||
<img src={logoImg} className="w-5 h-5" />
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Version 1.0.0
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{name ? (
|
||||
<div className="flex gap-2 items-center text-[#666] text-xs">
|
||||
<AppWindowMac className="w-5 h-5" /> {name}
|
||||
|
||||
@@ -1,52 +1,70 @@
|
||||
import { useState } from "react";
|
||||
import { Cloud } from "lucide-react";
|
||||
|
||||
import { UserProfile } from "./UserProfile";
|
||||
import { DataSourcesList } from "./DataSourcesList";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { ConnectService } from "./ConnectService";
|
||||
|
||||
export default function CocoCloud() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-2 px-4 py-2 bg-white rounded-md border border-gray-200">
|
||||
<Cloud className="w-5 h-5 text-blue-500" />
|
||||
<span className="font-medium">Coco Cloud</span>
|
||||
</div>
|
||||
<span className="px-3 py-1 text-sm text-blue-600 bg-blue-50 rounded-md">
|
||||
Available
|
||||
</span>
|
||||
</div>
|
||||
<button className="p-2 text-gray-500 hover:text-gray-700">
|
||||
<Cloud className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
<span>Service provision: INFINI Labs</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span>Version Number: v2.3.0</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span>Update time: 2023-05-12</span>
|
||||
</div>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
Coco Cloud provides users with a cloud storage and data integration
|
||||
platform that supports account registration and data source
|
||||
management. Users can integrate multiple data sources (such as
|
||||
Google Drive, yuque, GitHub, etc.), easily access and search for
|
||||
files, documents and codes across platforms, and achieve efficient
|
||||
data collaboration and management.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-medium text-gray-900 mb-4">Account Information</h2>
|
||||
<button className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [isConnect, setIsConnect] = useState(true);
|
||||
|
||||
<UserProfile name="Rain" email="an121245@gmail.com" />
|
||||
<DataSourcesList />
|
||||
</div>
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50">
|
||||
<Sidebar />
|
||||
|
||||
<main className="flex-1">
|
||||
{isConnect ? <div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-2 px-4 py-2 bg-white rounded-md border border-gray-200">
|
||||
<Cloud className="w-5 h-5 text-blue-500" />
|
||||
<span className="font-medium">Coco Cloud</span>
|
||||
</div>
|
||||
<span className="px-3 py-1 text-sm text-blue-600 bg-blue-50 rounded-md">
|
||||
Available
|
||||
</span>
|
||||
</div>
|
||||
<button className="p-2 text-gray-500 hover:text-gray-700">
|
||||
<Cloud className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
<span>Service provision: INFINI Labs</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span>Version Number: v2.3.0</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span>Update time: 2023-05-12</span>
|
||||
</div>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
Coco Cloud provides users with a cloud storage and data
|
||||
integration platform that supports account registration and data
|
||||
source management. Users can integrate multiple data sources (such
|
||||
as Google Drive, yuque, GitHub, etc.), easily access and search
|
||||
for files, documents and codes across platforms, and achieve
|
||||
efficient data collaboration and management.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Account Information
|
||||
</h2>
|
||||
{isLogin ? (
|
||||
<UserProfile name="Rain" email="an121245@gmail.com" />
|
||||
) : (
|
||||
<button className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors">
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLogin ? <DataSourcesList /> : null }
|
||||
</div>: <ConnectService />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
53
src/components/Auth/ConnectService.tsx
Normal file
53
src/components/Auth/ConnectService.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
export function ConnectService() {
|
||||
const [sourceName, setSourceName] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
console.log('Connecting Google Drive with name:', sourceName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-4xl">
|
||||
<div className="mb-8">
|
||||
<button className="flex items-center text-gray-600 hover:text-gray-900">
|
||||
<ArrowLeft className="w-5 h-5 mr-2" />
|
||||
<span>Connect Google Drive</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<p className="text-gray-600">
|
||||
Coco needs to obtain authorization from your Google Drive account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="sourceName" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Data Source Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="sourceName"
|
||||
value={sourceName}
|
||||
onChange={(e) => setSourceName(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Your Google Drive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link2, Trash2 } from 'lucide-react';
|
||||
import { Link2, Trash2 } from "lucide-react";
|
||||
|
||||
interface Account {
|
||||
email: string;
|
||||
@@ -18,21 +18,19 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
|
||||
<div className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src={`/icons/${type}.svg`}
|
||||
alt={name}
|
||||
className="w-6 h-6"
|
||||
/>
|
||||
<img src={`/icons/${type}.svg`} alt={name} className="w-6 h-6" />
|
||||
<span className="font-medium">{name}</span>
|
||||
</div>
|
||||
<button className="text-blue-500 hover:text-blue-600 flex items-center space-x-1">
|
||||
<Link2 className="w-4 h-4" />
|
||||
<span className="text-sm">{isConnected ? '管理' : '连接账户'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mb-2">
|
||||
{isConnected ? "Manage" : "Connect Accounts"}
|
||||
</div>
|
||||
|
||||
{accounts.map((account, index) => (
|
||||
<div
|
||||
<div
|
||||
key={account.email}
|
||||
className="flex items-center justify-between py-2 border-t border-gray-100"
|
||||
>
|
||||
@@ -44,14 +42,14 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
{index === 0 ? '我的网盘' : `网盘${index + 1}`}
|
||||
{index === 0 ? "My network disk" : `Network disk ${index + 1}`}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{account.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-xs text-gray-500">
|
||||
最近同步: {account.lastSync}
|
||||
Recently Synced: {account.lastSync}
|
||||
</span>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -61,4 +59,4 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ export function DataSourcesList() {
|
||||
name: 'Google Drive',
|
||||
type: 'google',
|
||||
accounts: [
|
||||
{ email: 'an121245@gmail.com', lastSync: '2025年1月2日 09:50 AM' },
|
||||
{ email: '9paiii@gmail.com', lastSync: '2025年1月2日 09:50 AM' }
|
||||
{ email: 'an121245@gmail.com', lastSync: '2025-01-02 09:50 AM' },
|
||||
{ email: '9paiii@gmail.com', lastSync: '2025-01-02 09:50 AM' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -27,7 +27,7 @@ export function DataSourcesList() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-medium text-gray-900">数据源</h2>
|
||||
<h2 className="text-xl font-medium text-gray-900">Data Source</h2>
|
||||
<div className="space-y-4">
|
||||
{dataSources.map(source => (
|
||||
<DataSourceItem key={source.id} {...source} />
|
||||
|
||||
28
src/components/Auth/Sidebar.tsx
Normal file
28
src/components/Auth/Sidebar.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Cloud, Plus } from "lucide-react";
|
||||
|
||||
export function Sidebar() {
|
||||
return (
|
||||
<div className="w-64 border-r border-gray-200 bg-white">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center space-x-2 px-3 py-2 bg-blue-50 text-blue-600 rounded-lg mb-6">
|
||||
<Cloud className="w-5 h-5" />
|
||||
<span className="font-medium">Coco Cloud</span>
|
||||
<div className="flex-1" />
|
||||
<button className="text-blue-600 hover:text-blue-700">
|
||||
<Cloud className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-gray-500 px-3 mb-2">
|
||||
Third-party services
|
||||
</div>
|
||||
|
||||
<button className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-200 rounded-lg text-gray-400 hover:text-gray-600 hover:border-gray-300">
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,6 @@ interface UserProfileProps {
|
||||
export function UserProfile({ name, email }: UserProfileProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-medium text-gray-900">账户信息</h2>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<User className="w-6 h-6 text-gray-500" />
|
||||
|
||||
59
src/components/Auth/callback.template.ts
Normal file
59
src/components/Auth/callback.template.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export default `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<title>Coco Auth</title>
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.container {
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.logo {
|
||||
width: 130px;
|
||||
height: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
font-size: 21px;
|
||||
line-height: 26px;
|
||||
color: #12161F;
|
||||
margin: 0;
|
||||
}
|
||||
.error {
|
||||
color: #dc2626;
|
||||
margin-top: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Coco<h1>
|
||||
<p id="message">You are now signed in. Please re-open the Coco desktop app to continue.</p>
|
||||
<div id="error-container"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
55
src/components/Auth/login2.tsx
Normal file
55
src/components/Auth/login2.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Github, Mail, Apple } from 'lucide-react';
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import { LoginForm } from '@/components/Auth/LoginForm';
|
||||
import { SocialButton } from '@/components/Auth/SocialButton';
|
||||
import { Divider } from '@/components/Auth/Divider';
|
||||
import { authWitheGithub } from '@/utils/index';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const uid = searchParams.get("uid");
|
||||
const code = searchParams.get("code");
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
}, [code])
|
||||
|
||||
function GithubClick() {
|
||||
uid && authWitheGithub(uid)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md bg-gray-800 rounded-xl p-8 shadow-2xl">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Welcome Back</h1>
|
||||
<p className="text-gray-400">Sign in to continue to Coco</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<SocialButton
|
||||
icon={<Github className="w-5 h-5" />}
|
||||
provider="GitHub"
|
||||
onClick={() => GithubClick()}
|
||||
/>
|
||||
<SocialButton
|
||||
icon={<Mail className="w-5 h-5" />}
|
||||
provider="Google"
|
||||
onClick={() => console.log('Google login')}
|
||||
/>
|
||||
<SocialButton
|
||||
icon={<Apple className="w-5 h-5" />}
|
||||
provider="Apple"
|
||||
onClick={() => console.log('Apple login')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<LoginForm onSubmit={(email, password) => console.log(email, password)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,133 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import * as shell from "@tauri-apps/plugin-shell";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { OpenBrowserURL } from "@/utils/index";
|
||||
import logoImg from "@/assets/32x32.png";
|
||||
import callbackTemplate from "@/components/Auth/callback.template";
|
||||
import { clientEnv } from "@/utils/env";
|
||||
|
||||
|
||||
export default function Account() {
|
||||
const app_uid = useAppStore((state) => state.app_uid);
|
||||
const setAppUid = useAppStore((state) => state.setAppUid);
|
||||
|
||||
const { auth, setAuth } = useAuthStore();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
const setupAuthListener = async () => {
|
||||
try {
|
||||
if (!auth) {
|
||||
// Replace the current route with signin
|
||||
// navigate("/signin", { replace: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to set up auth listener:", error);
|
||||
}
|
||||
};
|
||||
|
||||
setupAuthListener();
|
||||
|
||||
// Clean up logic on unmount
|
||||
return () => {
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
await invoke("plugin:oauth|stop");
|
||||
} catch (e) {
|
||||
// Ignore errors if no server is running
|
||||
}
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
|
||||
cleanup();
|
||||
};
|
||||
}, [auth]);
|
||||
|
||||
async function signIn() {
|
||||
let res: (url: URL) => void;
|
||||
|
||||
try {
|
||||
const stopListening = await listen(
|
||||
"oauth://url",
|
||||
(data: { payload: string }) => {
|
||||
if (!data.payload.includes("token")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlObject = new URL(data.payload);
|
||||
res(urlObject);
|
||||
}
|
||||
);
|
||||
|
||||
// Stop any existing OAuth server first
|
||||
try {
|
||||
await invoke("plugin:oauth|stop");
|
||||
} catch (e) {
|
||||
// Ignore errors if no server is running
|
||||
}
|
||||
|
||||
const port: string = await invoke("plugin:oauth|start", {
|
||||
config: {
|
||||
response: callbackTemplate,
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
},
|
||||
// Add a cleanup function to stop the server after handling the request
|
||||
cleanup: true,
|
||||
},
|
||||
});
|
||||
|
||||
await shell.open(
|
||||
`${clientEnv.COCO_SERVER_URL}/api/desktop/session/request?port=${port}`
|
||||
);
|
||||
|
||||
const url = await new Promise<URL>((r) => {
|
||||
res = r;
|
||||
});
|
||||
stopListening();
|
||||
|
||||
const token = url.searchParams.get("token");
|
||||
const user_id = url.searchParams.get("user_id");
|
||||
const expires = Number(url.searchParams.get("expires"));
|
||||
if (!token || !expires || !user_id) {
|
||||
throw new Error("Invalid token or expires");
|
||||
}
|
||||
|
||||
await setAuth({
|
||||
token,
|
||||
user_id,
|
||||
expires,
|
||||
plan: { upgraded: false, last_checked: 0 },
|
||||
});
|
||||
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch(() => {});
|
||||
|
||||
return navigate("/");
|
||||
} catch (error) {
|
||||
console.error("Sign in failed:", error);
|
||||
await setAuth(undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeUser() {
|
||||
let uid = app_uid;
|
||||
if (!uid) {
|
||||
@@ -22,6 +142,15 @@ export default function Account() {
|
||||
// const { token } = await response.json();
|
||||
// localStorage.setItem("auth_token", token);
|
||||
OpenBrowserURL(`http://localhost:1420/login?uid=${uid}`);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await signIn();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function LoginClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
|
||||
@@ -59,7 +188,7 @@ export default function Account() {
|
||||
className="text-sm/6 font-semibold text-gray-900 dark:text-gray-100"
|
||||
onClick={LoginClick}
|
||||
>
|
||||
Log In <span aria-hidden="true">→</span>
|
||||
{loading ? "Signing In..." : "Sign In"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import SettingsPanel from "./SettingsPanel";
|
||||
import GeneralSettings from "./GeneralSettings";
|
||||
import AboutView from "./AboutView";
|
||||
import Account from "./Account";
|
||||
// import CocoCloud from "@/components/Auth/CocoCloud"
|
||||
import CocoCloud from "@/components/Auth/CocoCloud"
|
||||
import Footer from "../Footer";
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { AppTheme } from "../../utils/tauri";
|
||||
@@ -25,7 +25,7 @@ function SettingsPage() {
|
||||
const tabs = [
|
||||
{ name: "General", icon: Settings },
|
||||
{ name: "Extensions", icon: Puzzle },
|
||||
{ name: "Account", icon: User },
|
||||
{ name: "Connect", icon: User },
|
||||
{ name: "Advanced", icon: Settings2 },
|
||||
{ name: "About", icon: Info },
|
||||
];
|
||||
@@ -79,7 +79,7 @@ function SettingsPage() {
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Account />
|
||||
{/* <CocoCloud /> */}
|
||||
<CocoCloud />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
|
||||
@@ -1,55 +1,124 @@
|
||||
import { Github, Mail, Apple } from 'lucide-react';
|
||||
import { useEffect } from "react";
|
||||
import { GoogleOAuthProvider } from "@react-oauth/google";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import { LoginForm } from '@/components/Auth/LoginForm';
|
||||
import { SocialButton } from '@/components/Auth/SocialButton';
|
||||
import { Divider } from '@/components/Auth/Divider';
|
||||
import { authWitheGithub } from '@/utils/index';
|
||||
import { useEffect } from 'react';
|
||||
import { authWitheGithub } from "@/utils/index";
|
||||
import loginImg from "@/assets/images/bg-login.png";
|
||||
import logoImg from "@/assets/images/coco-logo.png";
|
||||
import AppleImg from "@/assets/images/apple.png";
|
||||
import GithubImg from "@/assets/images/github.png";
|
||||
import GoogleImg from "@/assets/images/google.png";
|
||||
|
||||
export default function LoginPage() {
|
||||
const handleGoogleSignIn = (response: any) => {
|
||||
console.log("Google Login Success:", response);
|
||||
// response.credential
|
||||
};
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const uid = searchParams.get("uid");
|
||||
const code = searchParams.get("code");
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
}, [code])
|
||||
useEffect(() => {}, [code]);
|
||||
|
||||
function GithubClick() {
|
||||
uid && authWitheGithub(uid)
|
||||
function handleGithubSignIn() {
|
||||
uid && authWitheGithub(uid);
|
||||
}
|
||||
|
||||
const clientId = "YOUR_APPLE_CLIENT_ID";
|
||||
const redirectUri = "http://localhost:3000";
|
||||
const scope = "name email";
|
||||
|
||||
const handleAppleSignIn = () => {
|
||||
const authUrl = `https://appleid.apple.com/auth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code`;
|
||||
window.location.href = authUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md bg-gray-800 rounded-xl p-8 shadow-2xl">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Welcome Back</h1>
|
||||
<p className="text-gray-400">Sign in to continue to Coco</p>
|
||||
<div className="min-h-screen bg-black flex justify-center">
|
||||
<div className="min-h-screen container py-[30px] relative overflow-hidden">
|
||||
{/* Background Image */}
|
||||
<div className="absolute top-[60px] inset-0 z-0">
|
||||
<img
|
||||
src={loginImg}
|
||||
alt="Background"
|
||||
className="w-full h-full object-cover opacity-50"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-black/20" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<SocialButton
|
||||
icon={<Github className="w-5 h-5" />}
|
||||
provider="GitHub"
|
||||
onClick={() => GithubClick()}
|
||||
/>
|
||||
<SocialButton
|
||||
icon={<Mail className="w-5 h-5" />}
|
||||
provider="Google"
|
||||
onClick={() => console.log('Google login')}
|
||||
/>
|
||||
<SocialButton
|
||||
icon={<Apple className="w-5 h-5" />}
|
||||
provider="Apple"
|
||||
onClick={() => console.log('Apple login')}
|
||||
/>
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-[100%] px-[130px]">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center mb-[60px]">
|
||||
<img src={logoImg} alt="Coco" className="h-10 w-[127px]" />
|
||||
</div>
|
||||
|
||||
{/* Main Text */}
|
||||
<div className="text-left mb-[60px]">
|
||||
<h1 className="text-[64px] leading-[72px] font-bold text-white mb-4">
|
||||
INSERT
|
||||
<br />
|
||||
THE
|
||||
<br />
|
||||
STRAW
|
||||
</h1>
|
||||
<h2 className="text-[64px] leading-[72px] font-bold text-cyan-400 mb-6">
|
||||
LET'S BEGIN!
|
||||
</h2>
|
||||
<p className="text-white">
|
||||
With Coco AI, accessing your data is as easy as sipping fresh
|
||||
coconut juice.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Social Login Buttons */}
|
||||
<div className="font-bold text-[20px] text-white leading-[30px] text-left uppercase mb-5">
|
||||
Sign in With
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<GoogleOAuthProvider clientId="YOUR_GOOGLE_CLIENT_ID">
|
||||
<button
|
||||
className="w-[60px] h-[60px] bg-white hover:bg-gray-100 text-black rounded-full py-3 px-4 flex items-center justify-center space-x-2 transition-colors"
|
||||
onClick={handleGoogleSignIn}
|
||||
>
|
||||
<img
|
||||
src={GoogleImg}
|
||||
alt="Continue with Google"
|
||||
className="w-[29px] h-[29px]"
|
||||
/>
|
||||
</button>
|
||||
</GoogleOAuthProvider>
|
||||
|
||||
<button
|
||||
className="w-[60px] h-[60px] bg-white hover:bg-gray-100 text-black rounded-full py-3 px-4 flex items-center justify-center space-x-2 transition-colors"
|
||||
onClick={handleGithubSignIn}
|
||||
>
|
||||
<img
|
||||
src={AppleImg}
|
||||
alt="Continue with Apple"
|
||||
className="w-[26px] h-[30px]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="w-[60px] h-[60px] bg-white hover:bg-gray-100 text-black rounded-full py-3 px-4 flex items-center justify-center space-x-2 transition-colors"
|
||||
onClick={handleAppleSignIn}
|
||||
>
|
||||
<img
|
||||
src={GithubImg}
|
||||
alt="Continue with GitHub"
|
||||
className="w-[30px] h-[29px]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<LoginForm onSubmit={(email, password) => console.log(email, password)} />
|
||||
{/* Footer */}
|
||||
<div className="absolute bottom-8 w-full text-sm text-gray-500 flex justify-center">
|
||||
© {new Date().getFullYear()} Coco Labs. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
34
src/stores/authStore.ts
Normal file
34
src/stores/authStore.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export type Plan = {
|
||||
upgraded: boolean;
|
||||
last_checked: number;
|
||||
};
|
||||
|
||||
export type AuthStore = {
|
||||
token: string;
|
||||
user_id: string | null;
|
||||
expires: number;
|
||||
plan: Plan | null;
|
||||
};
|
||||
|
||||
export type IAuthStore = {
|
||||
auth: AuthStore | undefined;
|
||||
setAuth: (auth: AuthStore | undefined) => void;
|
||||
resetAuth: () => void;
|
||||
};
|
||||
|
||||
export const useAuthStore = create<IAuthStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
auth: undefined,
|
||||
setAuth: (auth) => set({ auth }),
|
||||
resetAuth: () => set({ auth: undefined }),
|
||||
}),
|
||||
{
|
||||
name: "auth-store",
|
||||
partialize: (state) => ({ auth: state.auth }),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user