diff --git a/app/components/general-layout.tsx b/app/components/general-layout.tsx index add18d6..01025de 100644 --- a/app/components/general-layout.tsx +++ b/app/components/general-layout.tsx @@ -159,10 +159,7 @@ const UploadFileForm = () => { state, removeFile, cancelAll, - } = useUppy({ - uploader: "tus", - endpoint: import.meta.env.VITE_PUBLIC_TUS_ENDPOINT, - }); + } = useUppy(); console.log({ state, files: getFiles() }); diff --git a/app/components/lib/uppy.ts b/app/components/lib/uppy.ts index 3dc123c..5fb54bc 100644 --- a/app/components/lib/uppy.ts +++ b/app/components/lib/uppy.ts @@ -8,7 +8,7 @@ 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"; +import {S5Client, HashProgressEvent} from "@lumeweb/s5-js"; const LISTENING_EVENTS = [ "upload", @@ -19,11 +19,7 @@ const LISTENING_EVENTS = [ "files-added" ] as const -export function useUppy({ - endpoint -}: { - endpoint: string -}) { +export function useUppy() { const sdk = useSdk() const [uploadLimit, setUploadLimit] = useState(0) @@ -98,8 +94,24 @@ export function useUppy({ const file = uppyInstance.current?.getFile(fileID) as UppyFile // @ts-ignore if (file.uploader === "tus") { + const hashProgressCb = (event: HashProgressEvent) => { + uppyInstance.current?.emit("preprocess-progress", file, { + uploadStarted: false, + bytesUploaded: 0, + preprocess: { + mode: "determinate", + message: "Hashing file...", + value: Math.round((event.total / event.total) * 100) + } + }) + } + const options = await sdk.protocols!().get(PROTOCOL_S5).getSdk().getTusOptions(file.data as File, {}, {onHashProgress: hashProgressCb}) uppyInstance.current?.setFileState(fileID, { - tus: await sdk.protocols!().get(PROTOCOL_S5).getSdk().getTusOptions(file.data as File) + tus: options, + meta: { + ...options.metadata, + ...file.meta, + } }) } } @@ -145,7 +157,7 @@ export function useUppy({ }) if (useTus) { - uppy.use(Tus, { endpoint: endpoint, limit: 6 }) + uppy.use(Tus, { limit: 6, parallelUploads: 10 }) uppy.addPreProcessor(tusPreprocessor) } @@ -195,7 +207,7 @@ export function useUppy({ }) } setState("idle") - }, [targetRef, endpoint, uploadLimit]) + }, [targetRef, uploadLimit]) useEffect(() => { return () => { diff --git a/app/data/account-provider.ts b/app/data/account-provider.ts new file mode 100644 index 0000000..154abd2 --- /dev/null +++ b/app/data/account-provider.ts @@ -0,0 +1,53 @@ +import type {DataProvider, UpdateParams, UpdateResponse, HttpError} from "@refinedev/core"; +import {SdkProvider} from "~/data/sdk-provider.js"; + +type AccountParams = { + email?: string; + password?: string; +} + +type AccountData = AccountParams; + +export const accountProvider: SdkProvider = { + getList: () => { + throw Error("Not Implemented") + }, + getOne: () => { + throw Error("Not Implemented") + }, + // @ts-ignore + async update( + params: UpdateParams, + ): Promise> { + if (params.variables.email && params.variables.password) { + const ret = await accountProvider.sdk?.account().updateEmail(params.variables.email, params.variables.password); + + if (ret) { + if (ret instanceof Error) { + return Promise.reject(ret) + } + } else { + return Promise.reject(); + } + + return { + data: + { + email: params.variables.email, + }, + }; + } + + // Return an empty response if params.variables is undefined + return { + data: {} as AccountParams, + }; + }, + create: () => { + throw Error("Not Implemented") + }, + deleteOne: () => { + throw Error("Not Implemented") + }, + getApiUrl: () => "", +} diff --git a/app/data/auth-provider.ts b/app/data/auth-provider.ts index 55188b6..0f207b0 100644 --- a/app/data/auth-provider.ts +++ b/app/data/auth-provider.ts @@ -1,4 +1,4 @@ -import type {AuthProvider} from "@refinedev/core" +import type {AuthProvider, UpdatePasswordFormTypes} from "@refinedev/core" import type { AuthActionResponse, @@ -8,7 +8,6 @@ import type { // @ts-ignore } from "@refinedev/core/dist/interfaces/bindings/auth" import {Sdk} from "@lumeweb/portal-sdk"; -import Cookies from 'universal-cookie'; import type {AccountInfoResponse} from "@lumeweb/portal-sdk"; export type AuthFormRequest = { @@ -32,134 +31,116 @@ export type Identity = { email: string; } -export class PortalAuthProvider implements RequiredAuthProvider { - constructor(apiUrl: string) { - this._sdk = Sdk.create(apiUrl); +export interface UpdatePasswordFormRequest extends UpdatePasswordFormTypes{ + currentPassword: string; +} - const methods: Array = [ - 'login', - 'logout', - 'check', - 'onError', - 'register', - 'forgotPassword', - 'updatePassword', - 'getPermissions', - 'getIdentity', - ]; +export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => { + const maybeSetupAuth = (): void => { + const jwt = sdk.account().jwtToken; + if (jwt) { + sdk.setAuthToken(jwt); + } + }; - methods.forEach((method) => { - this[method] = this[method]?.bind(this) as any; - }); - } + return { + async login(params: AuthFormRequest): Promise { + const ret = await sdk.account().login({ + email: params.email, + password: params.password, + }); - private _sdk: Sdk; + let redirectTo: string | undefined; - get sdk(): Sdk { - return this._sdk; - } - - public static create(apiUrl: string): AuthProvider { - return new PortalAuthProvider(apiUrl); - } - - async login(params: AuthFormRequest): Promise { - const ret = await this._sdk.account().login({ - email: params.email, - password: params.password, - }) - - let redirectTo: string | undefined; - - if (ret) { - redirectTo = params.redirectTo; - if (!redirectTo) { - redirectTo = ret ? "/dashboard" : "/login"; + if (ret) { + redirectTo = params.redirectTo; + if (!redirectTo) { + redirectTo = ret ? "/dashboard" : "/login"; + } + sdk.setAuthToken(sdk.account().jwtToken); } - this._sdk.setAuthToken(this._sdk.account().jwtToken); - } - return { - success: ret, - redirectTo, - }; - } + return { + success: ret, + redirectTo, + }; + }, - async logout(params: any): Promise { - let ret = await this._sdk.account().logout(); - return {success: ret, redirectTo: "/login"}; - } + async logout(params: any): Promise { + let ret = await sdk.account().logout(); + return {success: ret, redirectTo: "/login"}; + }, - async check(params?: any): Promise { - this.maybeSetupAuth(); + async check(params?: any): Promise { + const ret = await sdk.account().ping(); - const ret = await this._sdk.account().ping(); + if (ret) { + maybeSetupAuth(); + } - return {authenticated: ret, redirectTo: ret ? undefined : "/login"}; - } + return {authenticated: ret, redirectTo: ret ? undefined : "/login"}; + }, - async onError(error: any): Promise { - const cookies = new Cookies(); - return {logout: true}; - } + async onError(error: any): Promise { + return {logout: true}; + }, - async register(params: RegisterFormRequest): Promise { - const ret = await this._sdk.account().register({ - email: params.email, - password: params.password, - first_name: params.firstName, - last_name: params.lastName, - }); - return {success: ret, redirectTo: ret ? "/dashboard" : undefined}; - } + async register(params: RegisterFormRequest): Promise { + const ret = await sdk.account().register({ + email: params.email, + password: params.password, + first_name: params.firstName, + last_name: params.lastName, + }); + return {success: ret, redirectTo: ret ? "/dashboard" : undefined}; + }, - async forgotPassword(params: any): Promise { - return {success: true}; - } + async forgotPassword(params: any): Promise { + return {success: true}; + }, - async updatePassword(params: any): Promise { - return {success: true}; - } + async updatePassword(params: UpdatePasswordFormRequest): Promise { + maybeSetupAuth(); + const ret = await sdk.account().updatePassword(params.currentPassword, params.password as string); - async getPermissions(params?: Record): Promise { - return {success: true}; - } + if (ret) { + if (ret instanceof Error) { + return { + success: false, + error: ret + } + } - async getIdentity(params?: Identity): Promise { - this.maybeSetupAuth(); - const ret = await this._sdk.account().info(); + return { + success: true + } + } else { + return { + success: false + } + } + }, - if (!ret) { - return {identity: null}; - } + async getPermissions(params?: Record): Promise { + return {success: true}; + }, - const acct = ret as AccountInfoResponse; + async getIdentity(params?: Identity): Promise { + maybeSetupAuth(); + const ret = await sdk.account().info(); - return { - id: acct.id, - firstName: acct.first_name, - lastName: acct.last_name, - email: acct.email, - }; - } + if (!ret) { + return {identity: null}; + } - maybeSetupAuth(): void { - const cookies = new Cookies(); - const jwtCookie = cookies.get('auth_token'); - if (jwtCookie) { - this._sdk.setAuthToken(jwtCookie); - } - } -} + const acct = ret as AccountInfoResponse; -export interface RequiredAuthProvider extends AuthProvider { - login: AuthProvider['login']; - logout: AuthProvider['logout']; - check: AuthProvider['check']; - onError: AuthProvider['onError']; - register: AuthProvider['register']; - forgotPassword: AuthProvider['forgotPassword']; - updatePassword: AuthProvider['updatePassword']; - getPermissions: AuthProvider['getPermissions']; - getIdentity: AuthProvider['getIdentity']; -} + return { + id: acct.id, + firstName: acct.first_name, + lastName: acct.last_name, + email: acct.email, + }; + }, + }; +}; diff --git a/app/data/file-provider.ts b/app/data/file-provider.ts index 004d6b1..4d89673 100644 --- a/app/data/file-provider.ts +++ b/app/data/file-provider.ts @@ -1,10 +1,11 @@ import type { DataProvider } from "@refinedev/core"; +import {SdkProvider} from "~/data/sdk-provider.js"; -export const defaultProvider: DataProvider = { +export const fileProvider: SdkProvider = { getList: () => { throw Error("Not Implemented") }, getOne: () => { throw Error("Not Implemented") }, update: () => { throw Error("Not Implemented") }, create: () => { throw Error("Not Implemented") }, deleteOne: () => { throw Error("Not Implemented") }, getApiUrl: () => "", -} \ No newline at end of file +} diff --git a/app/data/providers.ts b/app/data/providers.ts new file mode 100644 index 0000000..ee47b55 --- /dev/null +++ b/app/data/providers.ts @@ -0,0 +1,31 @@ +import type {AuthProvider, DataProvider} from "@refinedev/core"; +import {fileProvider} from "~/data/file-provider.js"; +import {Sdk} from "@lumeweb/portal-sdk"; +import {accountProvider} from "~/data/account-provider.js"; +import type {SdkProvider} from "~/data/sdk-provider.js"; +import {createPortalAuthProvider} from "~/data/auth-provider.js"; + +interface DataProviders { + default: SdkProvider; + auth: AuthProvider; + + [key: string]: SdkProvider | AuthProvider; +} + +let providers: DataProviders; + +export function getProviders(sdk: Sdk) { + if (providers) { + return providers; + } + + accountProvider.sdk = sdk; + fileProvider.sdk = sdk; + providers = { + default: accountProvider, + auth: createPortalAuthProvider(sdk), + files: fileProvider, + }; + + return providers; +} diff --git a/app/data/resources.ts b/app/data/resources.ts new file mode 100644 index 0000000..d36d69e --- /dev/null +++ b/app/data/resources.ts @@ -0,0 +1,18 @@ +import type {ResourceProps} from "@refinedev/core"; + +const resources: ResourceProps[] = [ + { + name: 'account', + meta: { + dataProviderName: 'default', + } + }, + { + name: 'file', + meta: { + dataProviderName: 'files', + } + } +]; + +export default resources; diff --git a/app/data/sdk-provider.ts b/app/data/sdk-provider.ts new file mode 100644 index 0000000..c5fbcea --- /dev/null +++ b/app/data/sdk-provider.ts @@ -0,0 +1,6 @@ +import {DataProvider} from "@refinedev/core"; +import {Sdk} from "@lumeweb/portal-sdk"; + +export interface SdkProvider extends DataProvider { + sdk?: Sdk; +} diff --git a/app/root.tsx b/app/root.tsx index b5c203c..5f17966 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,24 +1,18 @@ -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; +import {Links, Meta, Outlet, Scripts, ScrollRestoration,} from "@remix-run/react"; import stylesheet from "./tailwind.css?url"; -import type { LinksFunction } from "@remix-run/node"; +import type {LinksFunction} from "@remix-run/node"; // Supports weights 200-800 import '@fontsource-variable/manrope'; import {Refine} from "@refinedev/core"; import routerProvider from "@refinedev/remix-router"; -import { defaultProvider } from "~/data/file-provider"; -import {PortalAuthProvider} from "~/data/auth-provider"; import { notificationProvider } from "~/data/notification-provider"; import {SdkContextProvider} from "~/components/lib/sdk-context"; import { Toaster } from "~/components/ui/toaster"; - +import {getProviders} from "~/data/providers.js"; +import {Sdk} from "@lumeweb/portal-sdk"; +import resources from "~/data/resources.js"; export const links: LinksFunction = () => [ { rel: "stylesheet", href: stylesheet }, @@ -44,19 +38,18 @@ export function Layout({children}: { children: React.ReactNode }) { } export default function App() { - const auth = PortalAuthProvider.create("https://alpha.pinner.xyz") + const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL) + const providers = getProviders(sdk); return ( - + diff --git a/app/routes/account.tsx b/app/routes/account.tsx index 5522249..dedf176 100644 --- a/app/routes/account.tsx +++ b/app/routes/account.tsx @@ -3,10 +3,11 @@ import { getZodConstraint, parseWithZod } from "@conform-to/zod"; import { DialogClose } from "@radix-ui/react-dialog"; import { Cross2Icon } from "@radix-ui/react-icons"; import { - BaseKey, - useGetIdentity, - useUpdate, - useUpdatePassword, + Authenticated, + BaseKey, + useGetIdentity, + useUpdate, + useUpdatePassword, } from "@refinedev/core"; import { useMemo, useState } from "react"; import { z } from "zod"; @@ -40,6 +41,7 @@ import { Input } from "~/components/ui/input"; import { UsageCard } from "~/components/usage-card"; import QRImg from "~/images/QR.png"; +import {UpdatePasswordFormRequest} from "~/data/auth-provider.js"; export default function MyAccount() { const { data: identity } = useGetIdentity<{ email: string }>(); @@ -52,7 +54,8 @@ export default function MyAccount() { }); return ( - + +

My Account

{ @@ -211,6 +214,7 @@ export default function MyAccount() {
+
); } @@ -247,10 +251,11 @@ const ChangeEmailForm = ({ currentValue }: { currentValue: string }) => { const data = Object.fromEntries(new FormData(e.currentTarget).entries()); console.log(identity); updateEmail({ - resource: "users", - id: identity?.id || "", + resource: "account", + id: "", values: { email: data.email.toString(), + password: data.password.toString(), }, }); }, @@ -292,7 +297,7 @@ const ChangeEmailForm = ({ currentValue }: { currentValue: string }) => { const ChangePasswordSchema = z .object({ - currentPassword: z.string().email(), + currentPassword: z.string(), newPassword: z.string(), retypePassword: z.string(), }) @@ -311,7 +316,7 @@ const ChangePasswordForm = () => { const { mutate: updatePassword } = useUpdatePassword<{ password: string }>(); const [form, fields] = useForm({ id: "login", - constraint: getZodConstraint(ChangeEmailSchema), + constraint: getZodConstraint(ChangePasswordSchema), onValidate({ formData }) { return parseWithZod(formData, { schema: ChangePasswordSchema }); }, @@ -322,7 +327,7 @@ const ChangePasswordForm = () => { const data = Object.fromEntries(new FormData(e.currentTarget).entries()); updatePassword({ - password: data.newPassword.toString(), + password: data.newPassword.toString(), }); }, }); @@ -408,10 +413,7 @@ const ChangeAvatarForm = () => { state, removeFile, cancelAll, - } = useUppy({ - uploader: "tus", - endpoint: import.meta.env.VITE_PUBLIC_TUS_ENDPOINT, - }); + } = useUppy(); console.log({ state, files: getFiles() }); diff --git a/package.json b/package.json index 415a038..0a92fb1 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,15 @@ "dev": "remix vite:dev", "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "preview": "vite preview", - "typecheck": "tsc" + "typecheck": "tsc", + "patch-package": "patch-package", + "postinstall": "patch-package" }, "dependencies": { "@conform-to/react": "^1.0.2", "@conform-to/zod": "^1.0.2", "@fontsource-variable/manrope": "^5.0.19", - "@lumeweb/portal-sdk": "0.0.0-20240318183202", + "@lumeweb/portal-sdk": "0.0.0-20240319140708", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", @@ -44,7 +46,6 @@ "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": { diff --git a/vite-env.d.ts b/vite-env.d.ts new file mode 100644 index 0000000..5f868a8 --- /dev/null +++ b/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_PORTAL_API_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +}