Refine Integration #13

Merged
pcfreak30 merged 9 commits from riobuenoDevelops/refine-integration into develop 2024-03-19 23:50:55 +00:00
11 changed files with 360 additions and 142 deletions
Showing only changes of commit 43ac8560cb - Show all commits

View File

@ -108,8 +108,8 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => logout()}>
<ExitIcon className="mr-2" />
Log Out
</DropdownMenuItem>
Logout
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -0,0 +1,18 @@
import React from "react";
import {Sdk} from "@lumeweb/portal-sdk";
export const SdkContext = React.createContext<
Partial<Sdk>
>({});
export const SdkContextProvider: React.FC< {sdk: Sdk, children: React.ReactNode}> = ({sdk, children}) => {
return (
<SdkContext.Provider value={sdk}>
{children}
</SdkContext.Provider>
);
};
export function useSdk(): Partial<Sdk>{
return React.useContext(SdkContext);
}

View File

@ -0,0 +1,63 @@
import Uppy, {BasePlugin, DefaultPluginOptions} from '@uppy/core';
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
import {S5Client} from "@lumeweb/s5-js";
import {AxiosProgressEvent} from "axios";
interface UppyFileUploadOptions extends DefaultPluginOptions {
sdk: Sdk;
}
export default class UppyFileUpload extends BasePlugin {
private _sdk: Sdk;
constructor(uppy: Uppy, opts?: UppyFileUploadOptions) {
super(uppy, opts);
this.id = opts?.id || 'file-upload';
this.type = 'uploader';
this._sdk = opts?.sdk as Sdk;
}
install() {
this.uppy.addUploader(this.handleUpload.bind(this));
}
private async handleUpload(fileIDs: string[]) {
for (const fileID of fileIDs) {
const file = this.uppy.getFile(fileID);
if (!file) {
continue;
}
// @ts-ignore
if (file.uploader !== 'file') {
continue;
}
const uploadLimit = await this._sdk.account().uploadLimit();
let data = file.data;
if (file.data instanceof Blob) {
data = new File([data], file.name, {type: file.type});
}
try {
await this._sdk.protocols().get<S5Client>(PROTOCOL_S5).getSdk().uploadFile(data as File, {
largeFileSize: uploadLimit,
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
uploader: this,
bytesUploaded: progressEvent.loaded,
bytesTotal: progressEvent.total,
})
}
});
this.uppy.emit('upload-success', file, {uploadURL: null});
} catch (err) {
this.uppy.emit('upload-error', file, err);
}
}
}
}

View File

@ -1,17 +1,14 @@
import Uppy, { type State, debugLogger } from "@uppy/core"
import Uppy, {debugLogger, type State, UppyFile} from "@uppy/core"
import Tus from "@uppy/tus"
import toArray from "@uppy/utils/lib/toArray"
import {
type ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from "react"
import DropTarget, { type DropTargetOptions } from "./uppy-dropzone"
import {type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState} from "react"
import DropTarget, {type DropTargetOptions} from "./uppy-dropzone"
import {useSdk} from "~/components/lib/sdk-context.js";
import UppyFileUpload from "~/components/lib/uppy-file-upload.js";
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
import {S5Client} from "@lumeweb/s5-js";
const LISTENING_EVENTS = [
"upload",
@ -23,12 +20,26 @@ const LISTENING_EVENTS = [
] as const
export function useUppy({
uploader,
endpoint
}: {
uploader: "tus"
endpoint: string
}) {
const sdk = useSdk()
const [uploadLimit, setUploadLimit] = useState<number>(0)
useEffect(() => {
async function getUploadLimit() {
try {
const limit = await sdk.account!().uploadLimit();
setUploadLimit(limit);
} catch (err) {
console.log('Error occured while fetching upload limit', err);
}
}
getUploadLimit();
}, []);
const inputRef = useRef<HTMLInputElement>(null)
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null)
const uppyInstance = useRef<Uppy>()
@ -82,7 +93,29 @@ export function useUppy({
useEffect(() => {
if (!targetRef) return
const uppy = new Uppy({ logger: debugLogger }).use(DropTarget, {
const tusPreprocessor = async (fileIDs: string[]) => {
for(const fileID of fileIDs) {
const file = uppyInstance.current?.getFile(fileID) as UppyFile
// @ts-ignore
if (file.uploader === "tus") {
uppyInstance.current?.setFileState(fileID, {
tus: await sdk.protocols!().get<S5Client>(PROTOCOL_S5).getSdk().getTusOptions(file.data as File)
})
}
}
}
const uppy = new Uppy({
logger: debugLogger,
onBeforeUpload: (files) => {
for (const file of Object.entries(files)) {
// @ts-ignore
file[1].uploader = file[1].size > uploadLimit ? "tus" : "file";
}
return true;
},
}).use(DropTarget, {
target: targetRef
} as DropTargetOptions)
@ -97,6 +130,25 @@ export function useUppy({
uppyInstance.current?.addFiles(files)
}
uppy.iteratePlugins((plugin) => {
uppy.removePlugin(plugin);
});
uppy.use(UppyFileUpload, { sdk: sdk as Sdk })
let useTus = false;
uppyInstance.current?.getFiles().forEach((file) => {
if (file.size > uploadLimit) {
useTus = true;
}
})
if (useTus) {
uppy.use(Tus, { endpoint: endpoint, limit: 6 })
uppy.addPreProcessor(tusPreprocessor)
}
// We clear the input after a file is selected, because otherwise
// change event is not fired in Chrome and Safari when a file
// with the same name is selected.
@ -109,12 +161,7 @@ export function useUppy({
}
})
switch (uploader) {
case "tus":
uppy.use(Tus, { endpoint: endpoint, limit: 6 })
break
default:
}
uppy.on("complete", (result) => {
if (result.failed.length === 0) {
@ -148,7 +195,7 @@ export function useUppy({
})
}
setState("idle")
}, [targetRef, endpoint, uploader])
}, [targetRef, endpoint, uploadLimit])
useEffect(() => {
return () => {

View File

@ -33,10 +33,8 @@ export type Identity = {
}
export class PortalAuthProvider implements RequiredAuthProvider {
private sdk: Sdk;
constructor(apiUrl: string) {
this.sdk = Sdk.create(apiUrl);
this._sdk = Sdk.create(apiUrl);
const methods: Array<keyof AuthProvider> = [
'login',
@ -55,9 +53,18 @@ export class PortalAuthProvider implements RequiredAuthProvider {
});
}
private _sdk: Sdk;
get sdk(): Sdk {
return this._sdk;
}
public static create(apiUrl: string): AuthProvider {
return new PortalAuthProvider(apiUrl);
}
async login(params: AuthFormRequest): Promise<AuthActionResponse> {
const cookies = new Cookies();
const ret = await this.sdk.account().login({
const ret = await this._sdk.account().login({
email: params.email,
password: params.password,
})
@ -65,11 +72,11 @@ export class PortalAuthProvider implements RequiredAuthProvider {
let redirectTo: string | undefined;
if (ret) {
cookies.set('jwt', this.sdk.account().jwtToken, {path: '/'});
redirectTo = params.redirectTo;
if (!redirectTo) {
redirectTo = ret ? "/dashboard" : "/login";
}
this._sdk.setAuthToken(this._sdk.account().jwtToken);
}
return {
@ -79,38 +86,25 @@ export class PortalAuthProvider implements RequiredAuthProvider {
}
async logout(params: any): Promise<AuthActionResponse> {
let ret = await this.sdk.account().logout();
if (ret) {
const cookies = new Cookies();
cookies.remove('jwt');
}
let ret = await this._sdk.account().logout();
return {success: ret, redirectTo: "/login"};
}
async check(params?: any): Promise<CheckResponse> {
const cookies = new Cookies();
this.maybeSetupAuth();
const jwtCookie = cookies.get('jwt');
if (jwtCookie) {
this.sdk.setAuthToken(jwtCookie);
}
const ret = await this.sdk.account().ping();
if (!ret) {
cookies.remove('jwt');
}
const ret = await this._sdk.account().ping();
return {authenticated: ret, redirectTo: ret ? undefined : "/login"};
}
async onError(error: any): Promise<OnErrorResponse> {
const cookies = new Cookies();
return {logout: true};
}
async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
const ret = await this.sdk.account().register({
const ret = await this._sdk.account().register({
email: params.email,
password: params.password,
first_name: params.firstName,
@ -132,7 +126,8 @@ export class PortalAuthProvider implements RequiredAuthProvider {
}
async getIdentity(params?: Identity): Promise<IdentityResponse> {
const ret = await this.sdk.account().info();
this.maybeSetupAuth();
const ret = await this._sdk.account().info();
if (!ret) {
return {identity: null};
@ -148,12 +143,16 @@ export class PortalAuthProvider implements RequiredAuthProvider {
};
}
public static create(apiUrl: string): AuthProvider {
return new PortalAuthProvider(apiUrl);
maybeSetupAuth(): void {
const cookies = new Cookies();
const jwtCookie = cookies.get('auth_token');
if (jwtCookie) {
this._sdk.setAuthToken(jwtCookie);
}
}
}
interface RequiredAuthProvider extends AuthProvider {
export interface RequiredAuthProvider extends AuthProvider {
login: AuthProvider['login'];
logout: AuthProvider['logout'];
check: AuthProvider['check'];

55
app/data/pinning.ts Normal file
View File

@ -0,0 +1,55 @@
interface PinningStatus {
id: string;
progress: number;
status: 'inprogress' | 'completed' | 'stale';
}
// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
class PinningProcess {
private static instances: Map<string, PinningStatus> = new Map();
static async pin(id: string): Promise<{ success: boolean; message: string }> {
if (PinningProcess.instances.has(id)) {
return { success: false, message: "ID is already being processed" };
}
const pinningStatus: PinningStatus = { id, progress: 0, status: 'inprogress' };
PinningProcess.instances.set(id, pinningStatus);
// Simulate async progress
(async () => {
for (let progress = 1; progress <= 100; progress++) {
await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (500 - 100 + 1)) + 100)); // Simulate time passing with random duration between 100 and 500
pinningStatus.progress = progress;
if (progress === 100) {
pinningStatus.status = 'completed';
}
}
})();
return { success: true, message: "Pinning process started" };
}
static *pollProgress(id: string): Generator<PinningStatus | null, void, unknown> {
let status = PinningProcess.instances.get(id);
while (status && status.status !== 'completed') {
yield status;
status = PinningProcess.instances.get(id);
}
yield status ?? null; // Yield the final status, could be null if ID doesn't exist
}
}
// Example usage:
// (async () => {
// const { success, message } = await PinningProcess.pin("123");
// console.log(message);
// if (success) {
// const progressGenerator = PinningProcess.pollProgress("123");
// let result = progressGenerator.next();
// while (!result.done) {
// console.log(result.value); // Log the progress
// result = progressGenerator.next();
// }
// }
// })();

View File

@ -15,6 +15,7 @@ import {Refine} from "@refinedev/core";
import {PortalAuthProvider} from "~/data/auth-provider.js";
import routerProvider from "@refinedev/remix-router";
import { defaultProvider } from "./data/file-provider";
import {SdkContextProvider} from "~/components/lib/sdk-context.js";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
@ -39,9 +40,10 @@ export function Layout({children}: { children: React.ReactNode }) {
}
export default function App() {
const auth = PortalAuthProvider.create("https://alpha.pinner.xyz")
return (
<Refine
authProvider={PortalAuthProvider.create("https://alpha.pinner.xyz")}
authProvider={auth}
routerProvider={routerProvider}
dataProvider={defaultProvider}
resources={[
@ -49,7 +51,9 @@ export default function App() {
{ name: 'users' }
]}
>
<Outlet/>
<SdkContextProvider sdk={(auth as PortalAuthProvider).sdk}>
<Outlet/>
</SdkContextProvider>
</Refine>
);
}

View File

@ -1,12 +1,23 @@
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { BaseKey, useGetIdentity, useUpdate, useUpdatePassword } from "@refinedev/core";
import {
BaseKey,
useGetIdentity,
useUpdate,
useUpdatePassword,
} from "@refinedev/core";
import { useState } from "react";
import { z } from "zod";
import { Field } from "~/components/forms";
import { GeneralLayout } from "~/components/general-layout";
import { AddIcon, CloudIcon, CrownIcon } from "~/components/icons";
import { ManagementCard, ManagementCardAvatar, ManagementCardContent, ManagementCardFooter, ManagementCardTitle } from "~/components/management-card";
import {
ManagementCard,
ManagementCardAvatar,
ManagementCardContent,
ManagementCardFooter,
ManagementCardTitle,
} from "~/components/management-card";
import { Button } from "~/components/ui/button";
import {
Dialog,
@ -51,10 +62,12 @@ export default function MyAccount() {
<ManagementCard>
<ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold">
{identity?.email}
{identity?.email}
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, changeEmail: true })}>
<Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, changeEmail: true })}>
<AddIcon />
Change Email Address
</Button>
@ -63,8 +76,8 @@ export default function MyAccount() {
<ManagementCard>
<ManagementCardTitle>Account Type</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold flex gap-x-2">
Lite Premium Account
<CrownIcon />
Lite Premium Account
<CrownIcon />
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
@ -82,7 +95,9 @@ export default function MyAccount() {
<PasswordDots className="mt-6" />
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, changePassword: true })}>
<Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, changePassword: true })}>
<AddIcon />
Change Password
</Button>
@ -91,35 +106,27 @@ export default function MyAccount() {
<ManagementCard>
<ManagementCardTitle>Two-Factor Authentication</ManagementCardTitle>
<ManagementCardContent>
Improve security by enabling 2FA.
Improve security by enabling 2FA.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, setupTwoFactor: true })}>
<Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, setupTwoFactor: true })}>
<AddIcon />
Enable Two-Factor Authorization
</Button>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Backup Key</ManagementCardTitle>
<ManagementCardContent>
Never share this code with anyone.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon />
Export Backup Key
</Button>
</ManagementCardFooter>
</ManagementCard>
</div>
<h2 className="font-bold my-8">More</h2>
<div className="grid grid-cols-3 gap-x-8">
<ManagementCard variant="accent">
<ManagementCardTitle>Invite a Friend</ManagementCardTitle>
<ManagementCardContent>Get 1 GB per friend invited for free (max 5 GB).</ManagementCardContent>
<ManagementCardContent>
Get 1 GB per friend invited for free (max 5 GB).
</ManagementCardContent>
<ManagementCardFooter>
<Button variant="accent" className="h-12 gap-x-2">
<Button variant="accent" className="h-12 gap-x-2">
<AddIcon />
Send Invitation
</Button>
@ -127,7 +134,9 @@ export default function MyAccount() {
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Read our Resources</ManagementCardTitle>
<ManagementCardContent>Navigate helpful articles or get assistance.</ManagementCardContent>
<ManagementCardContent>
Navigate helpful articles or get assistance.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon />
@ -137,7 +146,9 @@ export default function MyAccount() {
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Delete Account</ManagementCardTitle>
<ManagementCardContent>Once initiated, this action cannot be undone.</ManagementCardContent>
<ManagementCardContent>
Once initiated, this action cannot be undone.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2" variant="destructive">
<AddIcon />
@ -170,21 +181,22 @@ export default function MyAccount() {
);
}
const ChangeEmailSchema = z.object({
email: z.string().email(),
password: z.string(),
retypePassword: z.string(),
})
.superRefine((data, ctx) => {
if (data.password !== data.retypePassword) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["retypePassword"],
message: "Passwords do not match",
});
}
return true;
});
const ChangeEmailSchema = z
.object({
email: z.string().email(),
password: z.string(),
retypePassword: z.string(),
})
.superRefine((data, ctx) => {
if (data.password !== data.retypePassword) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["retypePassword"],
message: "Passwords do not match",
});
}
return true;
});
const ChangeEmailForm = ({
open,
@ -195,7 +207,7 @@ const ChangeEmailForm = ({
setOpen: (value: boolean) => void;
currentValue: string;
}) => {
const{ data: identity } = useGetIdentity<{ id: BaseKey }>();
const { data: identity } = useGetIdentity<{ id: BaseKey }>();
const { mutate: updateEmail } = useUpdate();
const [form, fields] = useForm({
id: "login",
@ -210,13 +222,13 @@ const ChangeEmailForm = ({
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
console.log(identity);
updateEmail({
resource: 'users',
resource: "users",
id: identity?.id || "",
values: {
email: data.email.toString()
}
})
}
email: data.email.toString(),
},
});
},
});
return (
@ -255,21 +267,22 @@ const ChangeEmailForm = ({
);
};
const ChangePasswordSchema = z.object({
currentPassword: z.string().email(),
newPassword: z.string(),
retypePassword: z.string(),
})
.superRefine((data, ctx) => {
if (data.newPassword !== data.retypePassword) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["retypePassword"],
message: "Passwords do not match",
});
}
return true;
});
const ChangePasswordSchema = z
.object({
currentPassword: z.string().email(),
newPassword: z.string(),
retypePassword: z.string(),
})
.superRefine((data, ctx) => {
if (data.newPassword !== data.retypePassword) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["retypePassword"],
message: "Passwords do not match",
});
}
return true;
});
const ChangePasswordForm = ({
open,
@ -290,11 +303,10 @@ const ChangePasswordForm = ({
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
updatePassword({
password: data.newPassword.toString()
});
updatePassword({
password: data.newPassword.toString(),
});
},
});
@ -390,6 +402,7 @@ const SetupTwoFactorDialog = ({
const PasswordDots = ({ className }: { className?: string }) => {
return (
<svg
aria-hidden="true"
width="219"
height="7"
viewBox="0 0 219 7"
@ -414,4 +427,4 @@ const PasswordDots = ({ className }: { className?: string }) => {
<circle cx="215.5" cy="3.5" r="3.5" fill="currentColor" />
</svg>
);
};
};

View File

@ -14,7 +14,7 @@
"@conform-to/react": "^1.0.2",
"@conform-to/zod": "^1.0.2",
"@fontsource-variable/manrope": "^5.0.19",
"@lumeweb/portal-sdk": "0.0.0-20240314110748",
"@lumeweb/portal-sdk": "0.0.0-20240318183202",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
@ -40,10 +40,10 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"react": "^18.2.0",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"universal-cookie": "^7.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
@ -62,6 +62,7 @@
"tailwindcss": "^3.4.1",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-plugin-node-polyfills": "^0.21.0",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {

View File

@ -0,0 +1,13 @@
diff --git a/node_modules/@uppy/tus/lib/index.js b/node_modules/@uppy/tus/lib/index.js
index 1e0a1bb..ba95bb5 100644
--- a/node_modules/@uppy/tus/lib/index.js
+++ b/node_modules/@uppy/tus/lib/index.js
@@ -506,7 +506,7 @@ function _getCompanionClientArgs2(file) {
}
async function _uploadFiles2(files) {
const filesFiltered = filterNonFailedFiles(files);
- const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered);
+ const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered).filter(file => file?.uploader == 'tus');
this.uppy.emit('upload-start', filesToEmit);
await Promise.allSettled(filesFiltered.map(file => {
if (file.isRemote) {

View File

@ -1,27 +1,32 @@
import { vitePlugin as remix } from "@remix-run/dev"
import { defineConfig } from "vite"
import {vitePlugin as remix} from "@remix-run/dev"
import {defineConfig} from "vite"
import tsconfigPaths from "vite-tsconfig-paths"
import {nodePolyfills} from 'vite-plugin-node-polyfills'
export default defineConfig({
plugins: [
remix({
ssr: false,
ignoredRouteFiles: ["**/*.css"]
}),
tsconfigPaths()
],
server: {
fs: {
// Restrict files that could be served by Vite's dev server. Accessing
// files outside this directory list that aren't imported from an allowed
// file will result in a 403. Both directories and files can be provided.
// If you're comfortable with Vite's dev server making any file within the
// project root available, you can remove this option. See more:
// https://vitejs.dev/config/server-options.html#server-fs-allow
allow: [
"app",
"node_modules/@fontsource-variable/manrope",
]
plugins: [
remix({
ssr: false,
ignoredRouteFiles: ["**/*.css"]
}),
tsconfigPaths(),
nodePolyfills({protocolImports: false}),
],
build: {
minify: false
},
server: {
fs: {
// Restrict files that could be served by Vite's dev server. Accessing
// files outside this directory list that aren't imported from an allowed
// file will result in a 403. Both directories and files can be provided.
// If you're comfortable with Vite's dev server making any file within the
// project root available, you can remove this option. See more:
// https://vitejs.dev/config/server-options.html#server-fs-allow
allow: [
"app",
"node_modules/@fontsource-variable/manrope",
]
}
}
}
})