├── .gitignore ├── LICENSE.md ├── README.md ├── lerna.json ├── package.json ├── packages ├── core │ ├── README.md │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ ├── config │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── services │ │ │ ├── authService.ts │ │ │ ├── fileStorage.ts │ │ │ ├── gqlService.ts │ │ │ └── index.ts │ │ └── utils │ │ │ └── index.ts │ └── tsconfig.json ├── ui-react │ ├── README.md │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ ├── components │ │ │ ├── Auth │ │ │ │ ├── Login.tsx │ │ │ │ ├── Signup.tsx │ │ │ │ ├── VerifyEmail.tsx │ │ │ │ └── index.ts │ │ │ ├── Theme │ │ │ │ ├── ThemeProvider.tsx │ │ │ │ ├── ThemeProvider.types.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── styles │ │ │ │ └── index.ts │ │ ├── context │ │ │ ├── Auth │ │ │ │ ├── QAuth.tsx │ │ │ │ ├── QAuth.types.ts │ │ │ │ └── index.ts │ │ │ ├── Gql │ │ │ │ ├── QGql.tsx │ │ │ │ ├── QGql.types.ts │ │ │ │ └── index.ts │ │ │ ├── Theme │ │ │ │ └── ThemeContext.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useAuth.ts │ │ │ ├── useGql.ts │ │ │ └── useTheme.ts │ │ ├── index.ts │ │ ├── themes │ │ │ ├── defaultTheme.ts │ │ │ └── index.ts │ │ └── utils │ │ │ └── index.ts │ └── tsconfig.json └── ui-vue │ ├── README.md │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ └── index.ts │ └── tsconfig.json ├── tsconfig.base.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore root node_modules (Yarn workspace will use a single node_modules at the root) 2 | node_modules/ 3 | 4 | # Ignore node_modules in all subdirectories 5 | **/node_modules/ 6 | 7 | # Ignore Yarn and npm cache files 8 | .yarn/ 9 | .pnp.* 10 | .yarnrc.yml 11 | npm-debug.log* 12 | yarn-error.log 13 | 14 | # Ignore build outputs 15 | **/dist/ 16 | 17 | # Ignore TypeScript build artifacts 18 | **/*.tsbuildinfo 19 | 20 | # Ignore IDE or OS-specific files 21 | .DS_Store 22 | .idea/ 23 | .vscode/ 24 | *.swp 25 | *.log 26 | 27 | 28 | # env variables 29 | .env -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [Quorini] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | 1. The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | 2. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 3. The Software may not be used to endorse or promote products derived from 24 | this Software without prior written permission. 25 | 26 | 4. For any distribution, you must include the original copyright notice and a 27 | copy of this license. 28 | 29 | 5. For modifications to this Software, you must include a prominent notice 30 | stating that you have changed the Software and the date of the change. 31 | 32 | [Optional additional clauses can be added here based on your needs] 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quorini SDK 2 |

This is JS SDK to integrate your Quorini project with web app.

3 | 4 | [![Design and run serverless cloud API](https://github.com/user-attachments/assets/d446a409-e5e9-47d1-aeaf-169f24ec5eec)](https://quorini.com/) 5 |

Define your data model visually, and deploy a fully-managed serverless backend in minutes.

6 | 7 |
Visit quorini.com for more details.
8 |
Visit quorini.app to start building new project.
9 | 10 | [![Product of the day at ProductHunt](https://github.com/user-attachments/assets/1c07c569-ba3d-46fe-adb7-c8133a339409)](https://www.producthunt.com/products/quorini#quorini) 11 | 12 | [![Test project with Live API](https://cdn.prod.website-files.com/669c3258841cd988fbcc2ed2/67281d081ffa915bbb7370d8_mutationcreate2-ezgif.com-video-to-gif-converter.gif)](https://quorini.app/) 13 | 14 | --- 15 | 16 | # Getting Started 17 | 18 | For more detail about packages 19 | 20 | [npm package @quorini/core](https://www.npmjs.com/package/@quorini/core) – is used to configure a project with the backend published by quorini.app. Quorini SDK enables developers to develop Quorini Backend-powered mobile and web apps. 21 | 22 | [README.md](/packages/core/README.md) 23 | 24 | 25 | [npm package @quorini/ui-react](https://www.npmjs.com/package/@quorini/ui-react) – leverages a range of functions and React hooks designed to seamlessly integrate with your React application configured using the `@quorini/core` package in conjunction with the backend services provided by [quorini.app](quorini.app) 26 | 27 | [README.md](/packages/ui-react/README.md) 28 | 29 | ## Installation 30 | 31 | ```ts 32 | npm install @quorini/core 33 | npm install @quorini/ui-react 34 | ``` 35 | 36 | ## Configuration of SDK 37 | 38 | 1. Go to [quorini.app](http://quorini.app) **"Live API"**. 39 | 2. Navigate to **"Tech Docs"** tab. 40 | 3. Copy types, queries and mutations and place in you codebase/repository.\ 41 | 3.1. `./src/quorini-types.d.ts`\ 42 | 3.2. `./src/quorini-queries.ts`\ 43 | 3.3. `./src/quorini-mutations.ts` 44 | 4. Inside `index.tsx` globally configure you SDK integration.\ 45 | 4.1. `projectId` can be copied from URL path of **"Live API"**.\ 46 | 4.2. `env` (optional) can be **"production"** or **"development"**. By default, it’s **"production"**.\ 47 | 4.3. `qglPaths` (optional) and values are from step 3. 48 | 49 | ```tsx 50 | // index.tsx 51 | ... 52 | import { QClient } from "@quorini/core" 53 | import * as queries from './src/quorini-queries' 54 | import * as mutations from './src/quorini-mutations' 55 | 56 | QClient.configure({ 57 | projectId: "YOUR_PROJECT_ID", 58 | env: "YOUR_PROJECT_ENV", 59 | gqlPaths: { 60 | queries, 61 | mutations, 62 | }, 63 | }) 64 | 65 | ... 66 | ``` 67 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "1.1.65", 4 | "npmClient": "yarn" 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quorini-ui", 3 | "private": true, 4 | "version": "1.1.18", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "devDependencies": { 9 | "@types/node": "^22.8.7", 10 | "lerna": "^8.1.9", 11 | "rollup": "^2.60.0", 12 | "typescript": "^4.5.0" 13 | }, 14 | "scripts": { 15 | "build": "yarn workspaces run build" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @quorini/core 2 | 3 | ## Description 4 | This package is used to configure a project with the backend published by [quorini.app](https://quorini.app/).\ 5 | Quorini SDK enables developers to develop Quorini Backend-powered mobile and web apps. 6 | 7 | For more information, visit [github repository readme](https://github.com/quorini/quorini-js-sdk#quorini-sdk). 8 | 9 | ## Installing 10 | To install this package, simply type add or install `@quorini/core` using your favorite package manager: 11 | 12 | ```ts 13 | npm install @quorini/core 14 | 15 | or 16 | yarn add @quorini/core 17 | 18 | or 19 | pnpm add @quorini/core 20 | ``` 21 | 22 | ## Getting Started 23 | The Quorini SDK is modulized by clients and functions\ 24 | To send a request, you only need to import the QClient and the functions you need: 25 | 26 | ```tsx 27 | // ES5 example 28 | const { QClient, login, logout } = require("@quorini/core"); 29 | ``` 30 | ```tsx 31 | // ES6+ example 32 | import { QClient, login, logout } from "@quorini/core"; 33 | ``` 34 | 35 | ## Usage 36 | ```tsx 37 | import { QClient } from "@quorini/core"; 38 | 39 | QClient.configure({ 40 | projectId: "YOUR_PROJECT_ID", // specify your project id 41 | env: 'YOUR_PROJECT_ENVIRONMENT', // for example, 'production' or 'development' 42 | // add other optional configuration 43 | }) 44 | ``` 45 | 46 | ### Async/await 47 | We recommend using await operator to wait for the promise returned by `login` operation as follows: 48 | ```tsx 49 | // async/await. 50 | try { 51 | const session = await login(username, password); 52 | // process session. 53 | } catch (error) { 54 | // error handling. 55 | } finally { 56 | // finally. 57 | } 58 | ``` 59 | 60 | Async-await is clean, concise, intuitive, easy to debug and has better error handling as compared to using Promise chains or callbacks. 61 | #### Promises 62 | You can also use Promise chaining to execute `login` operation. 63 | ```tsx 64 | login(username, password).then( 65 | (response) => { 66 | // process response. 67 | }, 68 | (error) => { 69 | // error handling. 70 | } 71 | ); 72 | ``` 73 | Promises can also be called using .catch() and .finally() as follows: 74 | ```tsx 75 | login(username, password) 76 | .then((data) => { 77 | // process data. 78 | }) 79 | .catch((error) => { 80 | // error handling. 81 | }) 82 | .finally(() => { 83 | // finally. 84 | }); 85 | ``` 86 | 87 | ## **Subscriptions module (coming soon)** 88 | 89 | ```tsx 90 | import { QStream } from "@quorini/core" 91 | 92 | const callbackFnEvent = (obj) => { user's code } 93 | 94 | QStream.on("event", callbackFnEvent) 95 | 96 | QStrema.on("connect", callbackFnConnect) 97 | QStrema.on("close", callbackFnClose) 98 | 99 | ... 100 | 101 | QStream.subscribe("onObjectCreate", "OPTIONAL_GQL_QUERY_FILTER", "OPTIONAL_SELECTORS") 102 | ``` 103 | 104 | ## **Storage (coming soon)** 105 | - **Public file (e.g. Image)** 106 | 107 | ```tsx 108 | import { Storage } from '@quorini/core' 109 | 110 | export function App() { 111 | return 112 | } 113 | ``` 114 | - **Private or Protected file (e.g. Image)** 115 | 116 | ```tsx 117 | import { Storage } from '@quorini/core' 118 | 119 | export function App() { 120 | return ( 121 | `protected/${identityId}/dog.jpg`} 124 | /> 125 | ) 126 | } 127 | ``` 128 | - **Error Handling (e.g. Image)** 129 | 130 | ```tsx 131 | import { Storage } from '@quorini/core' 132 | 133 | export function App() { 134 | return ( 135 | console.error(error)} 140 | /> 141 | ) 142 | } 143 | ``` 144 | 145 | ## Operations 146 | - login 147 | - logout 148 | - signup 149 | - verifyEmail 150 | - refreshAuthToken 151 | - sendInvitation 152 | - acceptInvitation 153 | - query 154 | - mutate 155 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@quorini/core", 3 | "version": "1.1.65", 4 | "main": "dist/index.cjs.js", 5 | "module": "dist/index.esm.js", 6 | "types": "dist/index.d.ts", 7 | "exports": { 8 | "require": { 9 | "types": "./dist/index.d.ts", 10 | "default": "./dist/index.cjs.js" 11 | }, 12 | "import": { 13 | "types": "./dist/index.d.ts", 14 | "default": "./dist/index.esm.js" 15 | } 16 | }, 17 | "keywords": [ 18 | "quorini", 19 | "react", 20 | "typescript", 21 | "graphQL", 22 | "core", 23 | "ui-react" 24 | ], 25 | "scripts": { 26 | "build": "rollup -c" 27 | }, 28 | "dependencies": { 29 | "@apollo/client": "^3.11.10", 30 | "bson": "^6.10.1", 31 | "graphql": "^16.9.0" 32 | }, 33 | "devDependencies": { 34 | "@types/axios": "^0.14.4", 35 | "@types/graphql": "^14.5.0", 36 | "rollup": "^2.60.0", 37 | "rollup-plugin-commonjs": "^10.1.0", 38 | "rollup-plugin-dts": "^6.1.1", 39 | "rollup-plugin-node-resolve": "^5.2.0", 40 | "rollup-plugin-typescript2": "^0.36.0", 41 | "typescript": "^4.5.0" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/quorini/quorini-js-sdk" 49 | }, 50 | "author": "ernst1202", 51 | "license": "MIT", 52 | "gitHead": "76424c713234e1e82c7c9605d9d7fc4015881659" 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import dts from 'rollup-plugin-dts'; 3 | 4 | export default [ 5 | { 6 | input: './src/index.ts', 7 | output: [ 8 | { 9 | file: 'dist/index.cjs.js', 10 | format: 'cjs', 11 | sourcemap: true, 12 | }, 13 | { 14 | file: 'dist/index.esm.js', 15 | format: 'esm', 16 | sourcemap: true, 17 | }, 18 | ], 19 | plugins: [ 20 | typescript({ 21 | tsconfig: './tsconfig.json' 22 | }) 23 | ], 24 | external: [] 25 | }, 26 | { 27 | input: './src/index.ts', // Path to your entry declaration file 28 | output: [{ file: 'dist/index.d.ts', format: 'esm' }], 29 | plugins: [dts()], 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /packages/core/src/config/index.ts: -------------------------------------------------------------------------------- 1 | // This will allow the React project to configure the values based on their environment variables 2 | interface Config { 3 | projectId: string, 4 | env?: 'production' | 'development', 5 | gqlPaths?: { 6 | queries: Record, 7 | mutations: Record, 8 | }, 9 | } 10 | 11 | const QClient = (() => { 12 | let config = {} as Config; 13 | 14 | const privateProdUrls = { 15 | apiUrl: "https://api.quorini.io", 16 | authApiUrl: "https://auth.quorini.io", 17 | }; 18 | 19 | /** 20 | * privateDevUrls should be used in SDK developing or testing. 21 | */ 22 | 23 | // const privateDevUrls = { 24 | // apiUrl: "https://h5ti6dtzyl.execute-api.us-west-2.amazonaws.com/development", 25 | // authApiUrl: "https://hth72i9z93.execute-api.us-west-2.amazonaws.com/development", 26 | // }; 27 | 28 | 29 | return { 30 | // Public configuration method for React projects to pass their environment variables 31 | configure(externalConfig: Config) { 32 | config = { ...externalConfig }; 33 | }, 34 | 35 | // Retrieve public configuration 36 | getConfig() { 37 | return config; 38 | }, 39 | 40 | // Internal method to retrieve private values based on mode 41 | getPrivate() { 42 | return privateProdUrls; 43 | // return privateDevUrls; // When enable privateDevUrls, this code line should be enabled as well. 44 | }, 45 | }; 46 | })(); 47 | 48 | // Export QClient for use in other parts of the SDK 49 | export { QClient }; 50 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './services'; 3 | export * from './utils'; -------------------------------------------------------------------------------- /packages/core/src/services/authService.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { QClient } from '../config'; 3 | import { SESSION_KEY } from '.'; 4 | 5 | // Create an Axios instance with default config 6 | const apiClient = axios.create({ 7 | baseURL: QClient.getPrivate().authApiUrl, // Default base URL; can override per request if needed 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | }); 12 | 13 | // login function 14 | export const login = async (username: string, password: string) => { 15 | let result:any = undefined; 16 | try { 17 | const authApiUrl = QClient.getPrivate().authApiUrl; 18 | const projectId = QClient.getConfig().projectId; 19 | const projectEnvironment = QClient.getConfig().env; 20 | const response = await apiClient.post(`${authApiUrl}/${projectId}/log-in${projectEnvironment === 'development' ? `?env=dev` : ''}`, { 21 | authOption: { username, password }, 22 | }); 23 | if (response.status === 200 && response.data.accessToken) { 24 | result = response.data; 25 | } 26 | return result; 27 | } catch (error) { 28 | throw error; 29 | } 30 | }; 31 | 32 | // signup function 33 | export const signup = async (username: string, password: string, code: string, signupFormData: any, usergroup: string) => { 34 | let result:any = undefined; 35 | try { 36 | const url = `${QClient.getPrivate().apiUrl}/${QClient.getConfig().projectId}/gql${QClient.getConfig().env === 'development' ? `?env=dev` : ''}` 37 | const response = await apiClient.post(url, { 38 | authOption: { username, password, invitationCode: code }, 39 | query: `mutation create($input: create${usergroup}Input!) { create${usergroup}(input: $input) { id }}`, 40 | variables: { 41 | input: { 42 | ...signupFormData, 43 | }, 44 | }, 45 | }); 46 | if (response.status === 200) { 47 | result = response.data; 48 | } 49 | return result; 50 | } catch (error) { 51 | throw error; 52 | } 53 | }; 54 | 55 | // Verify Email 56 | export const verifyEmail = async (code: string, username: string) => { 57 | let result:any = undefined; 58 | try { 59 | const authApiUrl = QClient.getPrivate().authApiUrl; 60 | const projectId = QClient.getConfig().projectId; 61 | const projectEnvironment = QClient.getConfig().env; 62 | const response = await apiClient.get(`${authApiUrl}/${projectId}/verify-email?code=${code}&username=${username.replace("+", "%2B")}${projectEnvironment === 'development' ? `?env=dev` : ''}`); 63 | if (response.status === 200) result = response.data; 64 | return result; 65 | } catch (error) { 66 | throw error; 67 | } 68 | }; 69 | 70 | // refresh auth token 71 | export const refreshAuthToken = async (refreshToken: any) => { 72 | let result:any = undefined; 73 | try { 74 | const authApiUrl = QClient.getPrivate().authApiUrl; 75 | const projectId = QClient.getConfig().projectId; 76 | const projectEnvironment = QClient.getConfig().env; 77 | const response = await apiClient.post(`${authApiUrl}/${projectId}/refresh-token/${projectEnvironment === 'development' ? `?env=dev` : ''}`, {refreshToken}); 78 | result = response.data; 79 | return result; 80 | } catch (error) { 81 | throw error; 82 | } 83 | } 84 | 85 | export const sendInvitation = async (email: string, userGroup: string, accessToken: any) => { 86 | let result:any = undefined; 87 | try { 88 | const authApiUrl = QClient.getPrivate().authApiUrl; 89 | const projectId = QClient.getConfig().projectId; 90 | const projectEnvironment = QClient.getConfig().env; 91 | const response = await apiClient.post(`${authApiUrl}/${projectId}/send-invitation${projectEnvironment === 'development' ? `?env=dev` : ''}`, 92 | { 93 | email, 94 | userGroup, 95 | }, 96 | { 97 | headers: { 98 | Authorization: accessToken, 99 | } 100 | } 101 | ); 102 | result = response.data; 103 | return result; 104 | } catch (error) { 105 | throw error; 106 | } 107 | } 108 | 109 | export const acceptInvitation = async (email: string, password: string, code: string) => { 110 | let result:any = undefined; 111 | try { 112 | const authApiUrl = QClient.getPrivate().authApiUrl; 113 | const projectId = QClient.getConfig().projectId; 114 | const projectEnvironment = QClient.getConfig().env; 115 | const response = await apiClient.post(`${authApiUrl}/${projectId}/accept-invitation${projectEnvironment === 'development' ? `?env=dev` : ''}`, 116 | { 117 | authOption: { 118 | email, 119 | newPassword: password, 120 | invitationCode: code, 121 | } 122 | } 123 | ); 124 | result = response.data; 125 | return result; 126 | } catch (error) { 127 | throw error; 128 | } 129 | } 130 | 131 | export const logout = () => { 132 | localStorage.removeItem(SESSION_KEY); 133 | } -------------------------------------------------------------------------------- /packages/core/src/services/fileStorage.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { QClient } from '../config'; 3 | import { SESSION_KEY } from '.'; 4 | 5 | // Create an Axios instance with default config 6 | const apiClient = axios.create({ 7 | baseURL: QClient.getPrivate().authApiUrl, // Default base URL; can override per request if needed 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | }); 12 | 13 | const getPresignedUrl = async (accessToken: string) => { 14 | let result: any = undefined; 15 | try { 16 | const apiUrl = QClient.getPrivate().apiUrl; 17 | const projectId = QClient.getConfig().projectId; 18 | const projectEnvironment = QClient.getConfig().env; 19 | const url = `${apiUrl}/${projectId}/file-upload-url/${projectEnvironment === 'development' ? `?env=dev` : ''}`; 20 | 21 | const response = await apiClient.get(url, { 22 | headers: { 23 | Authorization: `${accessToken}`, 24 | }, 25 | }); 26 | 27 | if (response.status === 200) { 28 | result = response.data; 29 | } 30 | return result; 31 | } catch (error) { 32 | throw error; 33 | } 34 | } 35 | 36 | export const fileUpload = async (file: File, accessToken: string) => { 37 | let result: any = undefined; 38 | try { 39 | const presignedUrl = await getPresignedUrl(accessToken); 40 | 41 | // Set the Content-Type based on the file's MIME type 42 | const headers = { 43 | 'Content-Type': file.type || 'application/octet-stream', // Fallback to generic type if file.type is empty 44 | }; 45 | 46 | const response = await apiClient.put(presignedUrl.uploadUrl, file, { headers }); 47 | 48 | if (response.status === 200) { 49 | result = { 50 | id: presignedUrl.fileId, 51 | url: presignedUrl.uploadUrl 52 | }; 53 | } 54 | return result; 55 | } catch (error) { 56 | console.error("Error uploading file:", error); 57 | throw error; 58 | } 59 | } 60 | 61 | export const create = async (file: File, formData: any, usergroup: string) => { 62 | let result: any = undefined; 63 | const session = JSON.parse(localStorage.getItem(SESSION_KEY)!); 64 | try { 65 | const apiUrl = QClient.getPrivate().apiUrl; 66 | const projectId = QClient.getConfig().projectId; 67 | const projectEnvironment = QClient.getConfig().env; 68 | const url = `${apiUrl}/${projectId}/gql${projectEnvironment === 'development' ? `?env=dev` : ''}`; 69 | const uploadedFileId = fileUpload(file, session?.accessToken); 70 | const response = await apiClient.post( 71 | url, 72 | { 73 | query: `mutation create($input: create${usergroup}Input!) { create${usergroup}(input: $input) { id }}`, 74 | variables: { 75 | input: { 76 | ...formData, 77 | fileField: `${uploadedFileId}`, 78 | }, 79 | } 80 | }, 81 | { 82 | headers: { 83 | Authorization: `${session?.accessToken}` 84 | } 85 | } 86 | ); 87 | if (response.status === 200) { 88 | result = response.data; 89 | } 90 | return result; 91 | } catch (error) { 92 | throw error; 93 | } 94 | } 95 | 96 | export const update = async (file: File, formData: any, usergroup: string) => { 97 | let result: any = undefined; 98 | const session = JSON.parse(localStorage.getItem(SESSION_KEY)!); 99 | try { 100 | const apiUrl = QClient.getPrivate().apiUrl; 101 | const projectId = QClient.getConfig().projectId; 102 | const projectEnvironment = QClient.getConfig().env; 103 | const url = `${apiUrl}/${projectId}/gql${projectEnvironment === 'development' ? `?env=dev` : ''}`; 104 | const uploadedFileId = fileUpload(file, session?.accessToken); 105 | const response = await apiClient.post( 106 | url, 107 | { 108 | query: `mutation update($input: update${usergroup}Input!) { update${usergroup}(input: $input) { id }}`, 109 | variables: { 110 | input: { 111 | ...formData, 112 | fileField: `${uploadedFileId}`, 113 | }, 114 | } 115 | }, 116 | { 117 | headers: { 118 | Authorization: `${session?.accessToken}` 119 | } 120 | } 121 | ); 122 | if (response.status === 200) { 123 | result = response.data; 124 | } 125 | return result; 126 | } catch (error) { 127 | throw error; 128 | } 129 | } -------------------------------------------------------------------------------- /packages/core/src/services/gqlService.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, HttpLink, gql, OperationVariables } from '@apollo/client'; 2 | import { QClient } from '../config'; 3 | import { SESSION_KEY } from '.'; 4 | import { parse, print, FieldNode, SelectionSetNode, Kind, DocumentNode, OperationDefinitionNode } from 'graphql'; 5 | 6 | // Function to extract allowed fields from selector input 7 | const parseSelectors = (selectors: string): Record => { 8 | const result: Record = {}; 9 | const stack: Record[] = [result]; 10 | 11 | selectors 12 | .trim() 13 | .split('\n') 14 | .map((line) => line.trim()) 15 | .filter((line) => line.length > 0) 16 | .forEach((line) => { 17 | if (line.endsWith('{')) { 18 | const fieldName = line.replace('{', '').trim(); 19 | stack[stack.length - 1][fieldName] = {}; 20 | stack.push(stack[stack.length - 1][fieldName]); 21 | } else if (line === '}') { 22 | stack.pop(); 23 | } else { 24 | stack[stack.length - 1][line] = true; 25 | } 26 | }); 27 | 28 | return result; 29 | }; 30 | 31 | // Function to filter or modify a SelectionSetNode based on allowed fields 32 | const filterSelectionSet = (selectionSet: SelectionSetNode, allowedFields: Record): SelectionSetNode => { 33 | const filteredSelections = selectionSet.selections 34 | .map((selection) => { 35 | if (selection.kind === Kind.FIELD) { 36 | const field = selection as FieldNode; 37 | const fieldName = field.name.value; 38 | 39 | if (allowedFields[fieldName]) { 40 | // If the field is allowed and has nested fields in allowedFields 41 | if (field.selectionSet && typeof allowedFields[fieldName] === 'object' && Object.keys(allowedFields[fieldName]).length > 0) { 42 | return { 43 | ...field, 44 | selectionSet: filterSelectionSet(field.selectionSet, allowedFields[fieldName]), 45 | }; 46 | } 47 | // If the field is allowed and no nested fields are specified, keep it as is (leaf field) 48 | return field; 49 | } 50 | } 51 | return null; // Remove unallowed fields 52 | }) 53 | .filter((selection) => selection !== null); 54 | 55 | return { 56 | kind: Kind.SELECTION_SET, 57 | selections: filteredSelections, 58 | } as SelectionSetNode; 59 | }; 60 | 61 | // (auth) query function 62 | export const query = async ( 63 | baseQuery: string, 64 | variables?: VarsType, 65 | selectors?: string 66 | ): Promise => { 67 | const session = JSON.parse(localStorage.getItem(SESSION_KEY)!); 68 | 69 | const client = new ApolloClient({ 70 | link: new HttpLink({ 71 | uri: `${QClient.getPrivate().apiUrl}/${QClient.getConfig().projectId}/gql${QClient.getConfig().env === 'development' ? `?env=dev` : ''}`, 72 | headers: { 73 | Authorization: `${session?.accessToken}`, 74 | }, 75 | }), 76 | cache: new InMemoryCache({ 77 | addTypename: false, 78 | }), 79 | }); 80 | 81 | let gqlQueryString = baseQuery; 82 | 83 | if (selectors) { 84 | try { 85 | // Parse the base query into an AST 86 | const ast: DocumentNode = parse(baseQuery); 87 | 88 | // Parse the selectors into a nested object 89 | const allowedFields = parseSelectors(selectors); 90 | 91 | // Modify the AST based on the allowed fields 92 | const modifiedAst: DocumentNode = { 93 | ...ast, 94 | definitions: ast.definitions.map((definition) => { 95 | if (definition.kind === Kind.OPERATION_DEFINITION) { 96 | const opDef = definition as OperationDefinitionNode; 97 | 98 | // Assume the first selection is the main query field (e.g., "listListings") 99 | const mainSelection = opDef.selectionSet.selections[0] as FieldNode; 100 | 101 | if (mainSelection && mainSelection.selectionSet) { 102 | // Filter the selection set based on the allowed fields 103 | const filteredSelectionSet = filterSelectionSet(mainSelection.selectionSet, allowedFields); 104 | 105 | return { 106 | ...opDef, 107 | selectionSet: { 108 | ...opDef.selectionSet, 109 | selections: [ 110 | { 111 | ...mainSelection, 112 | selectionSet: filteredSelectionSet, 113 | }, 114 | ], 115 | }, 116 | }; 117 | } 118 | } 119 | return definition; 120 | }), 121 | }; 122 | 123 | // Convert the modified AST back to a string 124 | gqlQueryString = print(modifiedAst); 125 | } catch (error) { 126 | console.error('Error parsing GraphQL query or selectors:', error); 127 | throw new Error('Invalid GraphQL query or selectors format.'); 128 | } 129 | } 130 | 131 | // Parse the updated query string 132 | const gqlQuery = gql(gqlQueryString); 133 | 134 | // Ensure variables are never undefined 135 | const safeVariables = variables ?? ({} as VarsType); 136 | 137 | try { 138 | const response = await client.query({ 139 | query: gqlQuery, 140 | variables: safeVariables, 141 | fetchPolicy: "no-cache", 142 | }); 143 | 144 | if (!response.data) { 145 | throw new Error(`Query response data for "${baseQuery}" is null or undefined.`); 146 | } 147 | 148 | // Unwrap the first level of the response.data 149 | return Object.values(response.data)[0] as ResponseType; 150 | } catch (error) { 151 | console.error('Apollo Query Error:', error); 152 | throw error; 153 | } 154 | }; 155 | 156 | // (auth) mutate function 157 | export const mutate = async ( 158 | baseMutation: string, 159 | variables: VarsType, 160 | ): Promise => { 161 | const session = JSON.parse(localStorage.getItem(SESSION_KEY)!); 162 | 163 | const client = new ApolloClient({ 164 | link: new HttpLink({ 165 | uri: `${QClient.getPrivate().apiUrl}/${QClient.getConfig().projectId}/gql${QClient.getConfig().env === 'development' ? `?env=dev` : ''}`, 166 | headers: { 167 | Authorization: `${session?.accessToken}`, 168 | }, 169 | }), 170 | cache: new InMemoryCache({ 171 | addTypename: false, 172 | }), 173 | }); 174 | 175 | const mutation = gql(baseMutation); 176 | 177 | try { 178 | const response = await client.mutate({ 179 | mutation, 180 | variables, 181 | fetchPolicy: 'no-cache', 182 | }); 183 | 184 | if (!response.data) { 185 | throw new Error(`Mutation response data for "${baseMutation}" is null or undefined.`); 186 | } 187 | 188 | // Unwrap the first level of the response.data 189 | return Object.values(response.data)[0] as ResponseType; 190 | } catch (error) { 191 | console.error(`Error during mutation "${baseMutation}":`, error); 192 | throw error; 193 | } 194 | }; 195 | 196 | // Public (no-auth) query function 197 | export const publicQuery = async ( 198 | baseQuery: string, 199 | variables?: VarsType, 200 | selectors?: string 201 | ): Promise => { 202 | const client = new ApolloClient({ 203 | link: new HttpLink({ 204 | uri: `${QClient.getPrivate().apiUrl}/${QClient.getConfig().projectId}/public/gql${QClient.getConfig().env === 'development' ? `?env=dev` : ''}`, // Adjust URI for public endpoint 205 | }), 206 | cache: new InMemoryCache({ 207 | addTypename: false, 208 | }), 209 | }); 210 | 211 | let gqlQueryString = baseQuery; 212 | 213 | if (selectors) { 214 | try { 215 | const ast: DocumentNode = parse(baseQuery); 216 | const allowedFields = parseSelectors(selectors); 217 | 218 | const modifiedAst: DocumentNode = { 219 | ...ast, 220 | definitions: ast.definitions.map((definition) => { 221 | if (definition.kind === Kind.OPERATION_DEFINITION) { 222 | const opDef = definition as OperationDefinitionNode; 223 | const mainSelection = opDef.selectionSet.selections[0] as FieldNode; 224 | 225 | if (mainSelection && mainSelection.selectionSet) { 226 | const filteredSelectionSet = filterSelectionSet(mainSelection.selectionSet, allowedFields); 227 | 228 | return { 229 | ...opDef, 230 | selectionSet: { 231 | ...opDef.selectionSet, 232 | selections: [ 233 | { 234 | ...mainSelection, 235 | selectionSet: filteredSelectionSet, 236 | }, 237 | ], 238 | }, 239 | }; 240 | } 241 | } 242 | return definition; 243 | }), 244 | }; 245 | 246 | gqlQueryString = print(modifiedAst); 247 | } catch (error) { 248 | console.error('Error parsing GraphQL query or selectors:', error); 249 | throw new Error('Invalid GraphQL query or selectors format.'); 250 | } 251 | } 252 | 253 | const gqlQuery = gql(gqlQueryString); 254 | const safeVariables = variables ?? ({} as VarsType); 255 | 256 | try { 257 | const response = await client.query({ 258 | query: gqlQuery, 259 | variables: safeVariables, 260 | fetchPolicy: 'no-cache', 261 | }); 262 | 263 | if (!response.data) { 264 | throw new Error(`Query response data for "${baseQuery}" is null or undefined.`); 265 | } 266 | 267 | // Unwrap the first level of the response.data 268 | return Object.values(response.data)[0] as ResponseType; 269 | } catch (error) { 270 | console.error('Apollo Public Query Error:', error); 271 | throw error; 272 | } 273 | }; 274 | 275 | // Public (no-auth) mutation function 276 | export const publicMutate = async ( 277 | baseMutation: string, 278 | variables: VarsType 279 | ): Promise => { 280 | const client = new ApolloClient({ 281 | link: new HttpLink({ 282 | uri: `${QClient.getPrivate().apiUrl}/${QClient.getConfig().projectId}/public/gql${QClient.getConfig().env === 'development' ? `?env=dev` : ''}`, // Adjust URI for public endpoint 283 | }), 284 | cache: new InMemoryCache({ 285 | addTypename: false, 286 | }), 287 | }); 288 | 289 | const mutation = gql(baseMutation); 290 | 291 | try { 292 | const response = await client.mutate({ 293 | mutation, 294 | variables, 295 | }); 296 | 297 | if (!response.data) { 298 | throw new Error(`Public mutation response data for "${baseMutation}" is null or undefined.`); 299 | } 300 | 301 | // Unwrap the first level of the response.data 302 | return Object.values(response.data)[0] as ResponseType; 303 | } catch (error) { 304 | console.error(`Error during public mutation "${baseMutation}":`, error); 305 | throw error; 306 | } 307 | }; -------------------------------------------------------------------------------- /packages/core/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authService'; 2 | export * from './gqlService'; 3 | export * from './fileStorage'; 4 | 5 | // export constant 6 | export const SESSION_KEY = 'qclient-session'; -------------------------------------------------------------------------------- /packages/core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export type Metadata = { 2 | requiredFields: string[]; 3 | optionalFields: string[]; 4 | protectedFields: string[]; 5 | deprecatedFields: string[]; 6 | fieldTypes: Record; 7 | enumFields: Record; 8 | }; 9 | 10 | export function extractMetadata>(schema: T): Metadata { 11 | const requiredFields: string[] = []; 12 | const optionalFields: string[] = []; 13 | const protectedFields: string[] = []; 14 | const deprecatedFields: string[] = []; 15 | const fieldTypes: Record = {}; 16 | const enumFields: Record = {}; 17 | 18 | for (const [key, value] of Object.entries(schema)) { 19 | const valueType = typeof value === "string" ? value : typeof value; 20 | 21 | // Handle protected and deprecated fields 22 | if (valueType.includes("protected")) { 23 | protectedFields.push(key); 24 | } 25 | if (valueType.includes("deprecated")) { 26 | deprecatedFields.push(key); 27 | } 28 | 29 | // Detect enum fields (like 'status') 30 | if (Array.isArray(value)) { 31 | enumFields[key] = value; 32 | } 33 | 34 | // Store the field type 35 | fieldTypes[key] = valueType; 36 | 37 | // Classify fields as required or optional 38 | if (key.endsWith("?")) { 39 | optionalFields.push(key.replace("?", "")); 40 | } else { 41 | requiredFields.push(key); 42 | } 43 | } 44 | 45 | return { requiredFields, optionalFields, protectedFields, deprecatedFields, fieldTypes, enumFields }; 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "types": ["node"], 6 | "paths": { 7 | "*": ["node_modules/*", "src/types/*"] 8 | } 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui-react/README.md: -------------------------------------------------------------------------------- 1 | # @quorini/ui-react 2 | 3 | ## Description 4 | This package leverages a range of functions and React hooks designed to seamlessly integrate with your React application configured using the [@quorini/core](https://www.npmjs.com/package/@quorini/core) package in conjunction with the backend services provided by [quorini.app](https://quorini.app/). 5 | 6 | ## Installing 7 | To install this package, simply type add or install `@quorini/ui-react` using your favorite package manager: 8 | 9 | ```ts 10 | npm install @quorini/ui-react 11 | 12 | or 13 | yarn add @quorini/ui-react 14 | 15 | or 16 | pnpm add @quorini/ui-react 17 | ``` 18 | 19 | ## Getting Started 20 | 21 | 22 | ```tsx 23 | // ES5 example 24 | const { QAuth, QGql } = require("@quorini/ui-react"); 25 | ``` 26 | ```tsx 27 | // ES6+ example 28 | import { QAuth, QGql } from "@quorini/ui-react"; 29 | ``` 30 | 31 | ## Usage 32 | 33 | For more information regarding configuration, visit [github repository readme](../../README.md) 34 | 35 | ### 1. Authenticator 36 | Before using the `@quorini/ui-react` package, ensure that your project is configured with the `@quorini/core` package. 37 | 38 | Additionally, all providers must wrap the root DOM of your React application, with `QAuth.Provider` being the first provider in the hierarchy. 39 | 40 | ```tsx 41 | /* src/index.tsx */ 42 | ... 43 | import { QClient } from "@quorini/core"; 44 | import { QAuth } from "@quorini/ui-react"; 45 | 46 | QClient.configure(...) 47 | 48 | const root = ReactDOM.createRoot( 49 | document.getElementById('root') as HTMLElement 50 | ) 51 | 52 | root.render( 53 | 54 | ... 55 | 56 | 57 | 58 | ... 59 | 60 | ) 61 | ``` 62 | 63 | `useAuth` Hook that can be used to access, modify, and update QAuth’s auth state. 64 | 65 | ```tsx 66 | /* src/app.tsx */ 67 | ... 68 | import { useAuth } from '@quorini/ui-react' 69 | 70 | function App() { 71 | const { session, logout } = useAuth() 72 | 73 | return ( 74 |
75 |

{session.username}

76 | 77 |
78 | ) 79 | } 80 | 81 | ``` 82 | 83 | ### **Option 1: Auth page as a single React UI component** 84 | 85 | - **Add Authenticator**\ 86 | 👉 Replace `CreateUserSignUpSchema` with actual mutation from the generated file `./quorini-mutations.ts` for the user profile creation form.\ 87 | 👉 Keep in mind that `CreateUserSignUpSchema` is only available for user groups with enabled sing-up. Change this in project configuration. 88 | 89 | The `QAuth.Provider` guarantees that the `useAuth` hook is available throughout your application. 90 | 91 | ```tsx 92 | // index.tsx 93 | ... 94 | import { QAuth } from '@quorini/ui-react' 95 | import '@quorini/ui-react/styles.css' 96 | 97 | import { CreateUserMetadata } from './quorini-mutations.ts' 98 | 99 | const root = ReactDOM.createRoot( 100 | document.getElementById('root') as HTMLElement 101 | ) 102 | 103 | root.render( 104 | 105 | ... 106 | 110 | 111 | 112 | ... 113 | 114 | ) 115 | ``` 116 | 117 | ### **Option 2 (custom UI): Auth module (for custom auth components implementation)** 118 | 119 | - **Add Auth Api endpoints**\ 120 | You can use the api endpoints directly. 121 | To use them, you need to import auth APIs from core package. 122 | 123 | ```tsx 124 | // auth.tsx 125 | ... 126 | import { login, signup, sendInvitation, refreshAuthToken } from '@quorini/core' 127 | 128 | const Auth = () => { 129 | const submitLogin = () => { 130 | const user = await login(username, password) 131 | } 132 | 133 | render() { 134 | return ( 135 |
136 | 137 | 138 |
89 | {selfSignup && (<>or Sign up now!)} 90 | 91 | 92 | 93 | ); 94 | }; 95 | 96 | const FormWrapper = styled.div` 97 | display: flex; 98 | justify-content: center; 99 | height: 100vh; 100 | align-items: center; 101 | `; 102 | 103 | export default Login; 104 | -------------------------------------------------------------------------------- /packages/ui-react/src/components/Auth/Signup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useAuth } from '../../hooks'; 3 | import styled from 'styled-components'; 4 | import { Alert, Button, Form, Input } from 'antd'; 5 | import { LockOutlined, UserOutlined, GiftOutlined } from "@ant-design/icons" 6 | import { MetaData } from '../../utils'; 7 | 8 | interface SignupProps { 9 | onSignupSuccess: () => void; 10 | onLoginClick: () => void; 11 | onAcceptSuccess: () => void; 12 | formFields?: MetaData[]; 13 | usergroup?: string; 14 | } 15 | 16 | const Signup: React.FC = ({ onSignupSuccess, onLoginClick, onAcceptSuccess, formFields, usergroup }) => { 17 | const { signup, acceptInvitation } = useAuth(); 18 | const [error, setError] = useState(null); 19 | const [isLoading, setIsLoading] = useState(false); 20 | const [form] = Form.useForm(); 21 | 22 | const pathname = window.location.pathname; 23 | if (pathname.includes("set-password")) { 24 | // Get the full URL 25 | const url = new URL(window.location.href); 26 | 27 | // Extract the query string 28 | const queryString = url.search; 29 | 30 | // Extract the fragment (part after #) 31 | const fragment = url.hash; 32 | 33 | // Parse the query string to get the parameters 34 | const params = new URLSearchParams(queryString); 35 | 36 | // Get the code and invitationEmail values 37 | let code = params.get("code") || ""; // Fallback to empty string if code is null 38 | const invitationEmail = params.get("email") || ""; // Fallback to empty string if email is null 39 | 40 | // Decode the code value to handle URL-encoded characters 41 | code = decodeURIComponent(code); 42 | 43 | // If there's a fragment, append it to the code 44 | if (fragment) { 45 | code += fragment; 46 | } 47 | 48 | const handleAcceptInvitation = (values: Record) => { 49 | setIsLoading(true); 50 | const { password } = values; 51 | if (!invitationEmail || invitationEmail.length === 0) { 52 | setError("email address is not valid!"); 53 | setIsLoading(false); 54 | return; 55 | } 56 | if (!password || password.length === 0) { 57 | setError("password is not valid!"); 58 | setIsLoading(false); 59 | return; 60 | } 61 | if (!code || code.length === 0) { 62 | setError("invitation code is not valid!"); 63 | setIsLoading(false); 64 | return; 65 | } 66 | acceptInvitation(invitationEmail, password, code) 67 | .then(() => { 68 | setIsLoading(false); 69 | onAcceptSuccess(); 70 | window.location.href = `${window.location.origin}/`; 71 | }) 72 | .catch(() => { 73 | setIsLoading(false); 74 | setError("sign up err. try again"); 75 | }); 76 | } 77 | 78 | if (invitationEmail) { 79 | return ( 80 | 81 |
82 | 83 | } 85 | size="large" 86 | style={{ width: "300px" }} 87 | defaultValue={invitationEmail} 88 | disabled 89 | /> 90 | 91 | 92 | 96 | } 98 | placeholder="Inviation Code" 99 | size="large" 100 | defaultValue={code || ''} 101 | disabled 102 | /> 103 | 104 | 105 | 123 | } 125 | placeholder="Password" 126 | size="large" 127 | /> 128 | 129 | 130 | {error && ( 131 | 132 | 133 | 134 | )} 135 | 136 | 137 | 140 | or Log in 141 | 142 |
143 |
144 | ) 145 | } 146 | } 147 | 148 | const handleSignup = (values: Record) => { 149 | setIsLoading(true); 150 | const {username, password, confirmPassword, ...rest} = values; 151 | 152 | if (password !== confirmPassword) { 153 | setError("Password not matched!"); 154 | setIsLoading(false); 155 | return; 156 | } 157 | signup(username, password, "", rest, usergroup || "") 158 | .then(() => { 159 | setIsLoading(false); 160 | onSignupSuccess(); 161 | }) 162 | .catch((err) => { 163 | setIsLoading(false); 164 | setError("sign up err. try again"); 165 | }); 166 | }; 167 | 168 | return ( 169 | 170 |
171 | 172 | } 174 | placeholder="Email address..." 175 | title="Email address..." 176 | size="large" 177 | style={{ width: "300px" }} 178 | autoFocus 179 | /> 180 | 181 | 182 | 200 | } 202 | placeholder="Password..." 203 | title="Password..." 204 | size="large" 205 | /> 206 | 207 | 208 | 226 | } 228 | placeholder="Confirm Password..." 229 | title="Confirm Password..." 230 | size="large" 231 | /> 232 | 233 | {formFields?.map((field) => ( 234 | 245 | 246 | 247 | ))} 248 | 249 | {error && ( 250 | 251 | 252 | 253 | )} 254 | 255 | 256 | 259 | or Log in 260 | 261 |
262 |
263 | ); 264 | }; 265 | 266 | const FormWrapper = styled.div` 267 | display: flex; 268 | justify-content: center; 269 | height: 100vh; 270 | align-items: center; 271 | `; 272 | 273 | export default Signup; 274 | -------------------------------------------------------------------------------- /packages/ui-react/src/components/Auth/VerifyEmail.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useAuth } from '../../hooks'; 3 | import { Alert, Button, Flex, Form, Input, Typography } from 'antd'; 4 | import styled from 'styled-components'; 5 | interface VerifyEmailProps { 6 | onVerifySuccess: () => void; 7 | }; 8 | 9 | const { Title } = Typography; 10 | 11 | const VerifyEmail: React.FC = ({ onVerifySuccess }) => { 12 | const { session, verifyEmail } = useAuth(); 13 | const [verificationCode, setVerificationCode] = useState(''); 14 | const [error, setError] = useState(null); 15 | const [isLoading, setIsLoading] = useState(false); 16 | 17 | const handleSubmit = async () => { 18 | setIsLoading(true); 19 | verifyEmail(verificationCode, session.username) 20 | .then(() => { 21 | setIsLoading(false); 22 | onVerifySuccess(); 23 | }) 24 | .catch((err) => { 25 | setIsLoading(false); 26 | setError("Verify Email Error. Try again later..."); 27 | }); 28 | }; 29 | 30 | return ( 31 | 32 |
37 | 38 | We sent you an email with the verification code. 39 | <br /> 40 | Please check your inbox (or spam in case you haven’t received it) 41 | 42 | 43 | setVerificationCode(e.target.value)} 48 | size="large" 49 | autoFocus 50 | /> 51 | 52 | 53 | {error && ( 54 | 55 | 56 | 57 | )} 58 | 59 | 60 | 67 | 74 | 75 |
76 |
77 | ); 78 | }; 79 | 80 | const FormWrapper = styled.div` 81 | display: flex; 82 | justify-content: center; 83 | height: 100vh; 84 | align-items: center; 85 | `; 86 | 87 | export default VerifyEmail; 88 | -------------------------------------------------------------------------------- /packages/ui-react/src/components/Auth/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Login } from './Login'; 2 | export { default as Signup } from './Signup'; 3 | export { default as VerifyEmail } from './VerifyEmail'; 4 | -------------------------------------------------------------------------------- /packages/ui-react/src/components/Theme/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | // Theme Provider -------------------------------------------------------------------------------- /packages/ui-react/src/components/Theme/ThemeProvider.types.ts: -------------------------------------------------------------------------------- 1 | // Theme Provider Types -------------------------------------------------------------------------------- /packages/ui-react/src/components/Theme/index.ts: -------------------------------------------------------------------------------- 1 | // Theme Provider Index -------------------------------------------------------------------------------- /packages/ui-react/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Auth'; 2 | // ... export others -------------------------------------------------------------------------------- /packages/ui-react/src/components/styles/index.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components" 2 | 3 | const Colors = { 4 | // Main background color – Slightly blueish-greenish almost white color | Cold white very very light gray 5 | background: "#eef2f6", 6 | transparentBackground: "rgba(238, 242, 246, 0.7)", 7 | transparentWhiteBackground: "rgba(255, 255, 255, 0.9)", 8 | transparentDarkFrostyBackground: "rgba(0, 0, 0, 0.25)", 9 | // Primary color – Dark gray almosst black 10 | primary: "#2c2c2c", 11 | primaryRed: "#d20101", 12 | // Even darker on hover 13 | primaryHover: "#1c1c1c", 14 | // 15 | grayDark: "#494b4d", 16 | grayNormal: "rgba(0, 10, 20, 0.35)", 17 | grayLight: "rgba(0, 5, 10, 0.06)", 18 | } 19 | 20 | const StyleHelpers = { 21 | boldBoxShadow: `0px 0px 15px 0px ${Colors.transparentDarkFrostyBackground}`, 22 | lightBoxShadow: `0px 0px 5px 0px ${Colors.transparentDarkFrostyBackground}`, 23 | staticBoxShadow: `0px 0px 2px 0px ${Colors.transparentDarkFrostyBackground}`, 24 | whiteGlowBoxShadow: `0 0 10px 3px ${Colors.transparentWhiteBackground}`, 25 | accentGlowShadow: `0 0 18px -8px ${Colors.primaryHover}`, 26 | blur: "blur(2px)", 27 | // 28 | radiusSmall: "10px", 29 | radiusMedium: "20px", 30 | // 31 | iconSize: "15px", 32 | largeIconSize: "25px", 33 | } 34 | 35 | const Spaces = { 36 | small: "5px", 37 | normal: "10px", 38 | medium: "15px", 39 | large: "20px", 40 | xLarge: "25px", 41 | } 42 | 43 | const Centered = styled.div<{ vertical?: boolean }>` 44 | display: flex; 45 | justify-content: center; 46 | align-items: center; 47 | width: 100%; 48 | flex-direction: ${(props) => (props.vertical ? "column" : "unset")}; 49 | gap: ${Spaces.large}; 50 | ` 51 | 52 | const PageWrapper = styled.div` 53 | display: flex; 54 | flex-direction: row; 55 | justify-content: center; 56 | width: 100%; 57 | min-height: 100vh; 58 | background-color: ${Colors.background}; 59 | /* padding: 10px 5px; */ 60 | ` 61 | 62 | const HalfContainer = styled.div<{ fullHeight?: boolean }>` 63 | flex: none; 64 | width: 50%; 65 | height: ${(props) => (props.fullHeight ? "100vh" : "unset")}; 66 | overflow-y: scroll; 67 | 68 | box-shadow: ${StyleHelpers.boldBoxShadow}; 69 | 70 | &:last-child { 71 | box-shadow: unset; 72 | padding: 0 0 0 ${Spaces.large}; 73 | } 74 | 75 | &:last-child:first-child { 76 | width: 100vw; 77 | padding: 0; 78 | } 79 | 80 | .ant-tabs-nav { 81 | background-color: white; 82 | box-shadow: ${StyleHelpers.lightBoxShadow}; 83 | z-index: 2; 84 | } 85 | ` 86 | 87 | const OneThirdContainer = styled(HalfContainer)` 88 | width: 30%; 89 | // min-width: 420px; 90 | ` 91 | 92 | const TwoThirdContainer = styled(HalfContainer)` 93 | width: 70%; 94 | // min-width: 980px; 95 | ` 96 | 97 | const ItemWithFadeInAnimation = styled.div<{ reversed?: boolean }>` 98 | opacity: 0; /* start with opacity set to 0 */ 99 | animation-name: fade-in; /* specify the animation */ 100 | animation-duration: 1s; /* specify the duration */ 101 | animation-fill-mode: forwards; /* keep the last frame of the animation */ 102 | @keyframes fade-in { 103 | from { 104 | opacity: 0; 105 | position: relative; 106 | // 40px is general for any list items 107 | // -20px is unique for NewAuthType component 108 | left: ${(props) => (props.reversed ? "-20px" : "40px")}; 109 | } 110 | to { 111 | opacity: 1; 112 | position: relative; 113 | left: 0px; 114 | } 115 | } 116 | ` 117 | 118 | // Define mixins 119 | const ScrollableBlock = css` 120 | overflow-y: scroll; 121 | overscroll-behavior: contain; 122 | 123 | &::before { 124 | content: ""; 125 | position: absolute; 126 | top: 0; 127 | left: 0; 128 | z-index: -1; 129 | width: 100%; 130 | height: 30px; 131 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.3); 132 | } 133 | 134 | &::after { 135 | content: ""; 136 | position: absolute; 137 | bottom: 0; 138 | left: 0; 139 | z-index: -1; 140 | width: 100%; 141 | height: 30px; 142 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.3); 143 | } 144 | ` 145 | 146 | export { 147 | Colors, 148 | Centered, 149 | PageWrapper, 150 | HalfContainer, 151 | OneThirdContainer, 152 | TwoThirdContainer, 153 | ItemWithFadeInAnimation, 154 | StyleHelpers, 155 | Spaces, 156 | ScrollableBlock, 157 | } 158 | -------------------------------------------------------------------------------- /packages/ui-react/src/context/Auth/QAuth.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useEffect } from 'react'; 2 | import { AuthContextType, AuthProviderProps, User } from './QAuth.types'; 3 | import { SESSION_KEY } from '@quorini/core'; 4 | import * as AuthService from '@quorini/core'; 5 | import { Login, Signup, VerifyEmail } from '../../components/Auth'; 6 | import { parseSchemaToFormFields } from '../../utils'; 7 | 8 | const AuthContext = createContext(undefined); 9 | 10 | interface QAuthProviderProps extends AuthProviderProps { 11 | LoginComponent?: React.ComponentType<{ onLoginSuccess: () => void }>; 12 | SignupComponent?: React.ComponentType<{ onSignupSuccess: () => void, }>; 13 | VerifyEmailComponent?: React.ComponentType<{ onVerifySuccess: () => void }>; 14 | signUpFormInputType?: Record, 15 | usergroup?: string, 16 | noLayout?: boolean, 17 | } 18 | 19 | const QAuthProvider: React.FC = ({ 20 | children, 21 | LoginComponent = Login, 22 | SignupComponent = Signup, 23 | VerifyEmailComponent = VerifyEmail, 24 | signUpFormInputType, 25 | usergroup, 26 | noLayout = false, 27 | }) => { 28 | const [authStep, setAuthStep] = useState<'login' | 'signup' | 'verifyEmail' | 'success'>('login'); 29 | const [session, setSession] = useState(null); 30 | 31 | useEffect(() => { 32 | const pathname = window.location.pathname; 33 | if (pathname.includes("set-password")) { 34 | localStorage.removeItem(SESSION_KEY) 35 | setAuthStep('signup'); 36 | } else { 37 | const session = JSON.parse(localStorage.getItem(SESSION_KEY)!); 38 | if (session) { 39 | setSession(session); 40 | } 41 | } 42 | }, []); 43 | 44 | const login = async (username: string, password: string) => { 45 | try { 46 | const sessionData = await AuthService.login(username, password); 47 | localStorage.setItem(SESSION_KEY, JSON.stringify({...sessionData, username})); 48 | setSession({...sessionData, username}); 49 | } catch (error) { 50 | setAuthStep('login'); 51 | throw error; 52 | } 53 | }; 54 | 55 | const signup = async (username: string, password: string, code: string, signupFormData: any, usergroup: string) => { 56 | try { 57 | await AuthService.signup(username, password, code, signupFormData, usergroup); 58 | } catch (error) { 59 | setAuthStep('signup'); 60 | throw error; 61 | } 62 | }; 63 | 64 | const verifyEmail = async (verificationCode: string, username: string) => { 65 | try { 66 | await AuthService.verifyEmail(verificationCode, username); 67 | } catch (error) { 68 | setAuthStep('verifyEmail'); 69 | throw error; 70 | } 71 | }; 72 | 73 | const refreshAuthToken = async () => { 74 | try { 75 | const updatedSession = await AuthService.refreshAuthToken(session.refreshToken); 76 | localStorage.setItem(SESSION_KEY, JSON.stringify({...updatedSession, username: session.username})); 77 | } catch (error) { 78 | throw error; 79 | } 80 | } 81 | 82 | const sendInvitation = async (email: string, usergroup: string) => { 83 | try { 84 | const session = JSON.parse(localStorage.getItem(SESSION_KEY)!); 85 | await AuthService.sendInvitation(email, usergroup, session.accessToken); 86 | } catch (error) { 87 | throw error; 88 | } 89 | } 90 | 91 | const acceptInvitation = async (email: string, password: string, code: string) => { 92 | try { 93 | await AuthService.acceptInvitation(email, password, code); 94 | } catch (error) { 95 | throw error; 96 | } 97 | } 98 | 99 | const logout = () => { 100 | localStorage.removeItem(SESSION_KEY); 101 | setAuthStep('login'); 102 | }; 103 | 104 | const renderAuthComponent = () => { 105 | if (noLayout || (session && session.accessToken)) { 106 | return children; 107 | } 108 | 109 | const signupFormFields = signUpFormInputType ? parseSchemaToFormFields(signUpFormInputType) : undefined; 110 | 111 | switch (authStep) { 112 | case 'signup': 113 | return ( 114 | setAuthStep('verifyEmail')} 118 | onAcceptSuccess={() => setAuthStep('login')} 119 | onLoginClick={() => setAuthStep('login')} 120 | /> 121 | ); 122 | case 'verifyEmail': 123 | return ( 124 | setAuthStep('login')} />); 125 | default: 126 | return ( 127 | setAuthStep('success')} 129 | onSignupClick={() => setAuthStep('signup')} 130 | selfSignup={!!signUpFormInputType} 131 | /> 132 | ); 133 | } 134 | }; 135 | 136 | return ( 137 | 149 | {renderAuthComponent()} 150 | 151 | ); 152 | }; 153 | 154 | // Export QAuth as an object with Provider property 155 | const QAuth = { 156 | Provider: QAuthProvider, 157 | }; 158 | 159 | export { QAuth, AuthContext }; 160 | -------------------------------------------------------------------------------- /packages/ui-react/src/context/Auth/QAuth.types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface AuthProviderProps { 4 | children: React.ReactNode; 5 | LoginComponent?: React.ComponentType<{ onLoginSuccess: () => void }>; 6 | SignupComponent?: React.ComponentType<{ onSignupSuccess: () => void }>; 7 | VerifyEmailComponent?: React.ComponentType<{ onVerifySuccess: () => void }>; 8 | } 9 | 10 | export interface AuthContextType { 11 | session: any | null; 12 | login: (username: string, password: string) => Promise; 13 | signup: (username: string, password: string, code: string, formData: any, usergroup: string) => Promise; 14 | logout: () => void; 15 | verifyEmail: (verificationCode: string, username: string) => Promise; 16 | sendInvitation: (email: string, usergroup: string) => Promise; 17 | acceptInvitation: (email: string, password: string, code: string) => Promise; 18 | refreshAuthToken: () => Promise; 19 | } 20 | 21 | export interface User { 22 | username: string; 23 | accessToken?: any; 24 | refreshToken?: any; 25 | } -------------------------------------------------------------------------------- /packages/ui-react/src/context/Auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './QAuth'; 2 | export * from './QAuth.types'; -------------------------------------------------------------------------------- /packages/ui-react/src/context/Gql/QGql.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode } from 'react'; 2 | import * as GqlService from '@quorini/core'; 3 | import { QGqlContextType } from './QGql.types'; 4 | 5 | // Create a context for GraphQL operations 6 | const QGqlContext = createContext(undefined); 7 | 8 | // Provider component to wrap your app 9 | export const QGqlProvider = ({ children }: { children: ReactNode }) => ( 10 | 11 | {children} 12 | 13 | ); 14 | 15 | const QGql = { 16 | Provider: QGqlProvider, 17 | }; 18 | 19 | export { QGql, QGqlContext }; 20 | -------------------------------------------------------------------------------- /packages/ui-react/src/context/Gql/QGql.types.ts: -------------------------------------------------------------------------------- 1 | import { OperationVariables } from "@apollo/client"; 2 | 3 | export interface ResponseType { 4 | [key: string]: any; 5 | } 6 | 7 | export interface QGqlContextType { 8 | query( 9 | operationName: string, 10 | variables?: VarsType, 11 | selectors?: string 12 | ): Promise; 13 | mutate( 14 | operationName: string, 15 | variables: VarsType, 16 | ): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui-react/src/context/Gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./QGql"; 2 | export * from "./QGql.types"; 3 | -------------------------------------------------------------------------------- /packages/ui-react/src/context/Theme/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | // Theme context -------------------------------------------------------------------------------- /packages/ui-react/src/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Auth"; 2 | export * from "./Gql"; 3 | // ... export others -------------------------------------------------------------------------------- /packages/ui-react/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useAuth } from './useAuth'; 2 | export { useQGql } from './useGql'; -------------------------------------------------------------------------------- /packages/ui-react/src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { AuthContext, AuthContextType } from '../context'; 3 | 4 | export const useAuth = (): AuthContextType => { 5 | const context = useContext(AuthContext); 6 | if (!context) { 7 | throw new Error('useAuth must be used within a QAuth.Provider'); 8 | } 9 | return context; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui-react/src/hooks/useGql.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { QGqlContextType, QGqlContext } from "../context"; 3 | 4 | export const useQGql = (): QGqlContextType => { 5 | const context = useContext(QGqlContext); 6 | if (!context) { 7 | throw new Error('useQGql must be used within a QGqlProvider'); 8 | } 9 | return context; 10 | }; -------------------------------------------------------------------------------- /packages/ui-react/src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | // theme hook -------------------------------------------------------------------------------- /packages/ui-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './context'; 3 | export * from './hooks'; 4 | export * from './themes'; -------------------------------------------------------------------------------- /packages/ui-react/src/themes/defaultTheme.ts: -------------------------------------------------------------------------------- 1 | // default theme -------------------------------------------------------------------------------- /packages/ui-react/src/themes/index.ts: -------------------------------------------------------------------------------- 1 | export {}; -------------------------------------------------------------------------------- /packages/ui-react/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export type MetaData = { 2 | name: string; 3 | required: boolean; 4 | label: string; 5 | placeholder: string; 6 | type: string; 7 | } 8 | 9 | export const parseSchemaToFormFields = (schema: Record): MetaData[] => { 10 | return Object.entries(schema).map(([key, value]) => { 11 | const isOptional = value.endsWith('?'); 12 | return { 13 | name: key, 14 | required: !isOptional, 15 | label: key.charAt(0).toUpperCase() + key.slice(1), 16 | placeholder: `Enter your ${key.charAt(0).toUpperCase() + key.slice(1)}`, 17 | type: value.replace('?', ''), // Remove "?" to identify the base type 18 | }; 19 | }); 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /packages/ui-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src"], 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui-vue/README.md: -------------------------------------------------------------------------------- 1 | # @quorini/ui-vue 2 | Upcoming... -------------------------------------------------------------------------------- /packages/ui-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@quorini/ui-vue", 3 | "version": "1.1.65", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "keywords": [ 7 | "quorini", 8 | "react", 9 | "typescript", 10 | "graphQL", 11 | "core", 12 | "ui-react" 13 | ], 14 | "scripts": { 15 | "build": "rollup -c" 16 | }, 17 | "peerDependencies": { 18 | "vue": "^3.0.0" 19 | }, 20 | "dependencies": { 21 | "@quorini/core": "^1.1.65" 22 | }, 23 | "devDependencies": { 24 | "rollup": "^2.60.0", 25 | "typescript": "^4.5.0" 26 | }, 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/quorini/quorini-js-sdk" 33 | }, 34 | "author": "ernst1202", 35 | "license": "MIT", 36 | "gitHead": "76424c713234e1e82c7c9605d9d7fc4015881659" 37 | } 38 | -------------------------------------------------------------------------------- /packages/ui-vue/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | 3 | export default { 4 | input: 'src/index.ts', 5 | output: { 6 | dir: 'dist', 7 | format: 'es', 8 | sourcemap: true, 9 | }, 10 | plugins: [ 11 | typescript({ 12 | tsconfig: './tsconfig.json' 13 | }) 14 | ], 15 | external: ['vue', '@quorini/core'] 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ui-vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export {}; -------------------------------------------------------------------------------- /packages/ui-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src"], 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "declaration": true, 7 | "declarationDir": "./dist", 8 | "jsx": "react", 9 | "outDir": "./dist", 10 | "baseUrl": ".", 11 | "moduleResolution": "node", 12 | "typeRoots": ["node_modules/@types"], 13 | "types": [], 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "lib": ["ESNext", "DOM"], 17 | "paths": { 18 | "@quorini/core": ["packages/core/src"], 19 | "@quorini/ui-react": ["packages/ui-react/src"], 20 | "@quorini/ui-vue": ["packages/ui-vue/src"] 21 | } 22 | }, 23 | "include": ["packages/*/src"], 24 | "exclude": ["node_modules", "dist"] 25 | } 26 | --------------------------------------------------------------------------------