├── .nojekyll ├── docsite ├── index.json ├── public │ ├── client │ │ ├── python.md │ │ ├── index.json │ │ └── js.md │ ├── apidoc │ │ ├── configure.md │ │ ├── index.json │ │ ├── install.md │ │ └── overview.md │ ├── examples │ │ ├── index.json │ │ ├── example1.md │ │ └── example2.md │ ├── server │ │ ├── index.json │ │ ├── go.md │ │ ├── python.md │ │ └── nodejs.md │ ├── favicon.ico │ ├── README.md │ └── img │ │ ├── quid-preview.jpg │ │ └── authentication-flow.png ├── src │ ├── assets │ │ └── index.css │ ├── views │ │ ├── HomeView.vue │ │ ├── ApidocView.vue │ │ ├── ExamplesView.vue │ │ ├── MdApiFileView.vue │ │ ├── MdClientFileView.vue │ │ ├── MdExampleFileView.vue │ │ ├── MdServerFileView.vue │ │ └── JsExamplesDetailView.vue │ ├── main.ts │ ├── env.d.ts │ ├── components │ │ ├── ExamplesList.vue │ │ ├── ClientList.vue │ │ ├── ServerList.vue │ │ ├── ApidocsList.vue │ │ ├── TheSidebar.vue │ │ └── TheHeader.vue │ ├── App.vue │ ├── conf.ts │ ├── widgets │ │ ├── RenderMdFile.vue │ │ └── RenderMd.vue │ ├── state.ts │ └── router.ts ├── postcss.config.js ├── .gitignore ├── README.md ├── index.html ├── tsconfig.json ├── tailwind.config.js ├── components.d.ts ├── vite.config.ts ├── LICENSE └── package.json ├── docs ├── 404.html ├── client │ ├── python.md │ ├── index.json │ └── js.md ├── apidoc │ ├── configure.md │ ├── index.json │ ├── install.md │ └── overview.md ├── examples │ ├── index.json │ ├── example1.md │ └── example2.md ├── server │ ├── index.json │ ├── go.md │ ├── python.md │ └── nodejs.md ├── favicon.ico ├── img │ ├── quid-preview.jpg │ └── authentication-flow.png ├── README.md ├── assets │ ├── ApidocView.2ce12063.js │ ├── MdApiFileView.84bb8040.js │ ├── MdClientFileView.da3eb11d.js │ ├── MdServerFileView.310b7195.js │ ├── MdExampleFileView.236940a8.js │ ├── ExamplesView.7e99d2a0.js │ └── RenderMdFile.d4ae6d4e.css └── index.html ├── Procfile ├── ui ├── src │ ├── components │ │ ├── user │ │ │ ├── SearchUser.vue │ │ │ ├── UserGroupsInfo.vue │ │ │ ├── AddUserIntoGroup.vue │ │ │ ├── AddUser.vue │ │ │ └── UserDatatable.vue │ │ ├── widgets │ │ │ ├── LoadingIndicator.vue │ │ │ ├── SimpleBadge.vue │ │ │ ├── cards │ │ │ │ ├── SimpleCard.vue │ │ │ │ └── CardsGrid.vue │ │ │ └── ActionButton.vue │ │ ├── namespace │ │ │ ├── TheCurrentNamespace.vue │ │ │ ├── NamespaceInfo.vue │ │ │ ├── NamespaceSelector.vue │ │ │ └── EditTokenTtl.vue │ │ ├── org │ │ │ ├── OrgDatatable.vue │ │ │ └── AddOrg.vue │ │ ├── group │ │ │ ├── AddGroup.vue │ │ │ └── GroupDatatable.vue │ │ ├── admin │ │ │ ├── AdminDatatable.vue │ │ │ └── add │ │ │ │ ├── subviews │ │ │ │ ├── SearchForUsers.vue │ │ │ │ └── SelectUser.vue │ │ │ │ └── AddAdmin.vue │ │ ├── TheSidebar.vue │ │ └── TheTopbar.vue │ ├── models │ │ ├── org │ │ │ ├── index.ts │ │ │ ├── contract.ts │ │ │ ├── interface.ts │ │ │ └── org.ts │ │ ├── group │ │ │ ├── index.ts │ │ │ ├── contract.ts │ │ │ ├── interface.ts │ │ │ └── group.ts │ │ ├── user │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ ├── contract.ts │ │ │ └── user.ts │ │ ├── category │ │ │ ├── index.ts │ │ │ └── category.ts │ │ ├── adminuser │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── interface.ts │ │ │ └── adminuser.ts │ │ ├── namespace │ │ │ ├── index.ts │ │ │ ├── contract.ts │ │ │ └── interface.ts │ │ └── siteuser │ │ │ ├── types.ts │ │ │ └── index.ts │ ├── assets │ │ ├── index.css │ │ └── logo.png │ ├── type.ts │ ├── packages │ │ ├── restmix │ │ │ ├── main.ts │ │ │ └── interfaces.ts │ │ └── quidjs │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── errors.ts │ ├── env.d.ts │ ├── views │ │ ├── HomeView.vue │ │ ├── OrgView.vue │ │ ├── NamespaceView.vue │ │ ├── UserView.vue │ │ ├── AdminsView.vue │ │ └── GroupView.vue │ ├── main.ts │ ├── conf.ts │ ├── env.ts │ ├── const │ │ └── categories.ts │ ├── interface.ts │ ├── router.ts │ ├── notify.ts │ ├── state.ts │ ├── api.ts │ └── App.vue ├── dist │ ├── favicon.ico │ └── index.html ├── public │ ├── favicon.ico │ └── img │ │ ├── logo.png │ │ ├── favicon.png │ │ ├── quid-preview.avif │ │ ├── quid-preview.jpg │ │ ├── logo.svg │ │ ├── logo-2em.svg │ │ └── logo-text.svg ├── postcss.config.js ├── tests │ ├── conf.ts │ ├── global-setup.ts │ ├── src │ │ └── admin │ │ │ ├── group.spec.ts │ │ │ ├── user.spec.ts │ │ │ ├── feat │ │ │ ├── group.ts │ │ │ └── user.ts │ │ │ ├── org.spec.ts │ │ │ ├── namespace.spec.ts │ │ │ └── nsadmin.spec.ts │ ├── playwright.init.config.js │ ├── init │ │ └── login.spec.ts │ ├── playwright.config.js │ ├── playwright.dev.config.js │ └── README.md ├── tsconfig.json ├── tailwind.config.js ├── index.html ├── vite.config.mts ├── package.json └── components.d.ts ├── doc ├── img │ ├── screenshot.png │ └── authentication-flow.png ├── dev_mode.md └── setup_db.md ├── docker-entrypoint-initdb.d └── init-user-db.sh ├── crypt ├── crypt_test.go └── crypt.go ├── .mailmap ├── .gitignore ├── server ├── db │ ├── errors.go │ ├── tokens.go │ ├── models.go │ ├── init.go │ ├── db.go │ └── nsadmin.go ├── api │ ├── quidadmin_middleware.go │ └── nsadmin_middleware.go ├── responses.go └── requests.go ├── .github ├── workflows │ ├── dependency-review.yml │ └── codeql-analysis.yml └── dependabot.yml ├── LICENSE ├── tokens └── claims.go ├── cmd └── quid │ └── conf.go ├── app.json └── go.mod /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docsite/index.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | index.html -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/quid -env -v 2 | -------------------------------------------------------------------------------- /docs/client/python.md: -------------------------------------------------------------------------------- 1 | ## Python client -------------------------------------------------------------------------------- /ui/src/components/user/SearchUser.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/apidoc/configure.md: -------------------------------------------------------------------------------- 1 | ## Configure 2 | 3 | -------------------------------------------------------------------------------- /docs/examples/index.json: -------------------------------------------------------------------------------- 1 | ["example1","example2"] -------------------------------------------------------------------------------- /docsite/public/client/python.md: -------------------------------------------------------------------------------- 1 | ## Python client -------------------------------------------------------------------------------- /docs/client/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "js", 3 | "python" 4 | ] -------------------------------------------------------------------------------- /docsite/public/apidoc/configure.md: -------------------------------------------------------------------------------- 1 | ## Configure 2 | 3 | -------------------------------------------------------------------------------- /docsite/public/examples/index.json: -------------------------------------------------------------------------------- 1 | ["example1","example2"] -------------------------------------------------------------------------------- /docs/apidoc/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "overview", 3 | "install" 4 | ] -------------------------------------------------------------------------------- /docsite/public/client/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "js", 3 | "python" 4 | ] -------------------------------------------------------------------------------- /docs/server/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "nodejs", 3 | "python", 4 | "go" 5 | ] -------------------------------------------------------------------------------- /docsite/public/apidoc/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "overview", 3 | "install" 4 | ] -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docsite/public/server/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "nodejs", 3 | "python", 4 | "go" 5 | ] -------------------------------------------------------------------------------- /ui/src/models/org/index.ts: -------------------------------------------------------------------------------- 1 | import Org from "./org"; 2 | 3 | export default Org; -------------------------------------------------------------------------------- /docs/examples/example1.md: -------------------------------------------------------------------------------- 1 | ## Example 1 2 | 3 | ```ts 4 | console.log("Hello") 5 | ``` -------------------------------------------------------------------------------- /ui/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/ui/dist/favicon.ico -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/src/models/group/index.ts: -------------------------------------------------------------------------------- 1 | import Group from "./group"; 2 | 3 | export default Group -------------------------------------------------------------------------------- /ui/src/models/user/index.ts: -------------------------------------------------------------------------------- 1 | import User from "./user"; 2 | 3 | export default { User } -------------------------------------------------------------------------------- /doc/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/doc/img/screenshot.png -------------------------------------------------------------------------------- /ui/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/ui/public/img/logo.png -------------------------------------------------------------------------------- /ui/src/assets/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/ui/src/assets/logo.png -------------------------------------------------------------------------------- /docs/img/quid-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/docs/img/quid-preview.jpg -------------------------------------------------------------------------------- /docsite/public/examples/example1.md: -------------------------------------------------------------------------------- 1 | ## Example 1 2 | 3 | ```ts 4 | console.log("Hello") 5 | ``` -------------------------------------------------------------------------------- /docsite/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/docsite/public/favicon.ico -------------------------------------------------------------------------------- /docsite/src/assets/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /ui/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/ui/public/img/favicon.png -------------------------------------------------------------------------------- /ui/src/models/category/index.ts: -------------------------------------------------------------------------------- 1 | import Category from "./category"; 2 | 3 | export default Category -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | Index page 4 | 5 | ```js 6 | const a = 1; 7 | console.log(a); 8 | ``` -------------------------------------------------------------------------------- /docsite/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /ui/src/models/adminuser/index.ts: -------------------------------------------------------------------------------- 1 | import AdminUser from "./adminuser"; 2 | 3 | export default AdminUser -------------------------------------------------------------------------------- /ui/src/models/adminuser/types.ts: -------------------------------------------------------------------------------- 1 | type UserType = "serverAdmin" | "nsAdmin"; 2 | 3 | export { UserType } -------------------------------------------------------------------------------- /ui/src/models/namespace/index.ts: -------------------------------------------------------------------------------- 1 | import Namespace from "./namespace"; 2 | 3 | export default Namespace -------------------------------------------------------------------------------- /ui/src/models/siteuser/types.ts: -------------------------------------------------------------------------------- 1 | type UserType = "serverAdmin" | "nsAdmin"; 2 | 3 | export { UserType } -------------------------------------------------------------------------------- /doc/img/authentication-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/doc/img/authentication-flow.png -------------------------------------------------------------------------------- /ui/public/img/quid-preview.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/ui/public/img/quid-preview.avif -------------------------------------------------------------------------------- /ui/public/img/quid-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/ui/public/img/quid-preview.jpg -------------------------------------------------------------------------------- /docs/img/authentication-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/docs/img/authentication-flow.png -------------------------------------------------------------------------------- /docsite/public/README.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | Index page 4 | 5 | ```js 6 | const a = 1; 7 | console.log(a); 8 | ``` -------------------------------------------------------------------------------- /docsite/public/img/quid-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/docsite/public/img/quid-preview.jpg -------------------------------------------------------------------------------- /docsite/public/img/authentication-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LM4eu/quid/HEAD/docsite/public/img/authentication-flow.png -------------------------------------------------------------------------------- /ui/src/models/org/contract.ts: -------------------------------------------------------------------------------- 1 | interface OrgContract { 2 | id: number; 3 | name: string; 4 | } 5 | 6 | export default OrgContract; -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | ] 6 | } -------------------------------------------------------------------------------- /docsite/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | ] 6 | } -------------------------------------------------------------------------------- /ui/src/type.ts: -------------------------------------------------------------------------------- 1 | type ColorVariant = "primary" | "secondary" | "neutral" | "light" | "success" | "warning" | "danger"; 2 | 3 | export { ColorVariant } -------------------------------------------------------------------------------- /ui/src/models/user/interface.ts: -------------------------------------------------------------------------------- 1 | interface UserTable { 2 | id: number; 3 | name: string; 4 | actions: Array; 5 | } 6 | 7 | export { UserTable } -------------------------------------------------------------------------------- /ui/tests/conf.ts: -------------------------------------------------------------------------------- 1 | const adminUser = { 2 | name: "admin", 3 | pwd: "adminpwd" 4 | }; 5 | 6 | const testNs = "testns"; 7 | 8 | export { adminUser, testNs } -------------------------------------------------------------------------------- /ui/src/models/group/contract.ts: -------------------------------------------------------------------------------- 1 | interface GroupContract { 2 | id: number; 3 | name: string; 4 | namespace: string; 5 | } 6 | 7 | export { GroupContract } -------------------------------------------------------------------------------- /ui/src/models/group/interface.ts: -------------------------------------------------------------------------------- 1 | interface GroupTable { 2 | id: number; 3 | name: string; 4 | actions: Array; 5 | } 6 | 7 | export { GroupTable } -------------------------------------------------------------------------------- /docsite/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | !/doc/dist 6 | *.local 7 | yarn.lock 8 | yarn-error.log 9 | .yarnrc 10 | /dev 11 | .vscode -------------------------------------------------------------------------------- /ui/src/models/org/interface.ts: -------------------------------------------------------------------------------- 1 | import OrgContract from "./contract"; 2 | 3 | interface OrgTable extends OrgContract { 4 | actions: Array; 5 | } 6 | 7 | export { OrgTable } -------------------------------------------------------------------------------- /docsite/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './assets/index.css'; 4 | import router from './router'; 5 | 6 | createApp(App).use(router).mount('#app') 7 | -------------------------------------------------------------------------------- /docs/examples/example2.md: -------------------------------------------------------------------------------- 1 | ## Example 2 2 | 3 | ```ts 4 | interface Hello { 5 | name: string 6 | }; 7 | 8 | const a: Hello = { name: "Bob" }; 9 | console.log(`Hello ${a}`); 10 | "Result: " + a 11 | ``` -------------------------------------------------------------------------------- /ui/src/packages/restmix/main.ts: -------------------------------------------------------------------------------- 1 | import { useApi } from "./api"; 2 | import { UseApiParams, ApiResponse, OnResponseHook } from "./interfaces"; 3 | 4 | export { useApi, UseApiParams, ApiResponse, OnResponseHook }; -------------------------------------------------------------------------------- /docsite/public/examples/example2.md: -------------------------------------------------------------------------------- 1 | ## Example 2 2 | 3 | ```ts 4 | interface Hello { 5 | name: string 6 | }; 7 | 8 | const a: Hello = { name: "Bob" }; 9 | console.log(`Hello ${a}`); 10 | "Result: " + a 11 | ``` -------------------------------------------------------------------------------- /docsite/README.md: -------------------------------------------------------------------------------- 1 | # Quid docsite 2 | 3 | ## Install 4 | 5 | Install the dependencies: 6 | 7 | ``` 8 | yarn 9 | ``` 10 | 11 | ## Run in dev mode 12 | 13 | ``` 14 | yarn dev 15 | ``` 16 | 17 | Open localhost:3000 18 | -------------------------------------------------------------------------------- /ui/src/models/user/contract.ts: -------------------------------------------------------------------------------- 1 | import NamespaceContract from "../namespace/contract"; 2 | 3 | interface UserContract { 4 | id: number; 5 | name: string; 6 | namespace: NamespaceContract; 7 | } 8 | 9 | export default UserContract; 10 | -------------------------------------------------------------------------------- /ui/src/packages/quidjs/index.ts: -------------------------------------------------------------------------------- 1 | import QuidRequests from "./model"; 2 | import { QuidRequestError } from "./errors"; 3 | import { QuidParams, QuidLoginParams } from "./types"; 4 | 5 | export { QuidRequestError, QuidRequests, QuidParams, QuidLoginParams } -------------------------------------------------------------------------------- /docs/assets/ApidocView.2ce12063.js: -------------------------------------------------------------------------------- 1 | import{d as e,c as s,a,b as o,o as t,_ as c}from"./index.9fbb40e9.js";const _={class:"p-3"},n=o("div",{class:"pb-3 text-xl"},"Api docs",-1),l=e({__name:"ApidocView",setup(d){return(i,p)=>(t(),s("div",_,[n,a(c)]))}});export{l as default}; 2 | -------------------------------------------------------------------------------- /docsite/src/views/ApidocView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /doc/dev_mode.md: -------------------------------------------------------------------------------- 1 | # Run in dev mode 2 | 3 | Use two consoles. 4 | 5 | ## Run the backend 6 | 7 | make run 8 | 9 | ## Run the frontend 10 | 11 | make run-front 12 | 13 | Open to login into the admin interface: 14 | 15 | xdg-open http://localhost:3000 16 | -------------------------------------------------------------------------------- /docsite/src/views/ExamplesView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /ui/src/models/adminuser/interface.ts: -------------------------------------------------------------------------------- 1 | interface AdminUserContract { 2 | "id": number; 3 | "usr_id": number; 4 | "ns_id": number; 5 | "name": string 6 | } 7 | 8 | interface AdminUserTable { 9 | id: number; 10 | name: string; 11 | usrId: number; 12 | } 13 | 14 | export { AdminUserContract, AdminUserTable } 15 | -------------------------------------------------------------------------------- /ui/src/models/namespace/contract.ts: -------------------------------------------------------------------------------- 1 | import { AlgoType } from "@/interface"; 2 | 3 | interface NamespaceContract { 4 | id: number; 5 | name: string; 6 | alg: AlgoType; 7 | max_access_ttl: string; 8 | max_refresh_ttl: string; 9 | public_endpoint_enabled: boolean; 10 | } 11 | 12 | export default NamespaceContract 13 | -------------------------------------------------------------------------------- /docs/apidoc/install.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | Download the latest [release](https://github.com/teal-finance/quid/releases) to run a binary or clone the repository to compile from source. See also the [Dockerfile](Dockerfile) to run **Quid** within a light container (less than 20 MB). 4 | 5 | ### Build from source 6 | 7 | ```bash 8 | make all -j 9 | ``` -------------------------------------------------------------------------------- /ui/src/models/namespace/interface.ts: -------------------------------------------------------------------------------- 1 | import { AlgoType } from "@/interface"; 2 | 3 | interface NamespaceTable { 4 | id: number; 5 | name: string; 6 | algo: AlgoType; 7 | maxTokenTtl: string; 8 | maxRefreshTokenTtl: string; 9 | publicEndpointEnabled: boolean; 10 | actions: Array; 11 | } 12 | 13 | export default NamespaceTable -------------------------------------------------------------------------------- /docsite/public/apidoc/install.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | Download the latest [release](https://github.com/teal-finance/quid/releases) to run a binary or clone the repository to compile from source. See also the [Dockerfile](Dockerfile) to run **Quid** within a light container (less than 20 MB). 4 | 5 | ### Build from source 6 | 7 | ```bash 8 | make all -j 9 | ``` -------------------------------------------------------------------------------- /docsite/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '*.vue' { 5 | import { DefineComponent } from 'vue' 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 7 | const component: DefineComponent<{}, {}, any> 8 | export default component 9 | } 10 | -------------------------------------------------------------------------------- /docsite/src/components/ExamplesList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docsite/src/components/ClientList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docsite/src/components/ServerList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/components/widgets/LoadingIndicator.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /docsite/src/components/ApidocsList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /ui/tests/global-setup.ts: -------------------------------------------------------------------------------- 1 | import { chromium, FullConfig } from '@playwright/test'; 2 | 3 | async function globalSetup(config: FullConfig) { 4 | console.log("Run global setup") 5 | const browser = await chromium.launch(); 6 | const page = await browser.newPage(); 7 | await page.goto("http://localhost:8090/"); 8 | 9 | await browser.close(); 10 | } 11 | 12 | export default globalSetup; 13 | -------------------------------------------------------------------------------- /docsite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Quid 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ui/src/components/widgets/SimpleBadge.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /ui/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | interface ImportMetaEnv { 11 | readonly VITE_DEV_TOKEN: string 12 | } 13 | 14 | interface ImportMeta { 15 | readonly env: ImportMetaEnv 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/packages/quidjs/types.ts: -------------------------------------------------------------------------------- 1 | interface QuidParams { 2 | quidUri: string; 3 | serverUri: string; 4 | namespace: string; 5 | timeouts: Record; 6 | credentials?: string | null; 7 | verbose: boolean; 8 | accessTokenUri?: string | null; 9 | onHasToLogin?: CallableFunction | null; 10 | } 11 | 12 | interface QuidLoginParams { 13 | username: string; 14 | password: string; 15 | refreshTokenTtl: string; 16 | } 17 | 18 | export { QuidParams, QuidLoginParams }; -------------------------------------------------------------------------------- /ui/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /docs/assets/MdApiFileView.84bb8040.js: -------------------------------------------------------------------------------- 1 | import{_ as r}from"./RenderMdFile.a0539a70.js";import{d as s,r as o,w as l,c,a as i,u as n,o as u,H as f,e as _}from"./index.9fbb40e9.js";const m={class:"container p-3 mx-auto"},v=s({__name:"MdApiFileView",setup(p){const t=o("");return l(()=>{var e,a;t.value=(a=(e=_.currentRoute.value.params)==null?void 0:e.file.toString())!=null?a:""}),(e,a)=>(u(),c("div",m,[i(r,{"file-url":`/apidoc/${t.value}.md`,hljs:n(f)},null,8,["file-url","hljs"])]))}});export{v as default}; 2 | -------------------------------------------------------------------------------- /ui/src/models/category/category.ts: -------------------------------------------------------------------------------- 1 | import { UserType } from "../siteuser/types"; 2 | 3 | export default class Category { 4 | title: string; 5 | icon: string; 6 | url: string | null; 7 | type: UserType; 8 | 9 | constructor({ title, icon, url = null, type = "serverAdmin" }: { 10 | title: string, icon: string, url?: string | null, type: UserType 11 | }) { 12 | this.title = title; 13 | this.icon = icon; 14 | this.url = url; 15 | this.type = type 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/assets/MdClientFileView.da3eb11d.js: -------------------------------------------------------------------------------- 1 | import{_ as r}from"./RenderMdFile.a0539a70.js";import{d as s,r as l,w as o,c as n,a as c,u as i,o as u,H as f,e as _}from"./index.9fbb40e9.js";const m={class:"container p-3 mx-auto"},v=s({__name:"MdClientFileView",setup(p){const t=l("");return o(()=>{var e,a;t.value=(a=(e=_.currentRoute.value.params)==null?void 0:e.file.toString())!=null?a:""}),(e,a)=>(u(),n("div",m,[c(r,{"file-url":`/client/${t.value}.md`,hljs:i(f)},null,8,["file-url","hljs"])]))}});export{v as default}; 2 | -------------------------------------------------------------------------------- /docs/assets/MdServerFileView.310b7195.js: -------------------------------------------------------------------------------- 1 | import{_ as t}from"./RenderMdFile.a0539a70.js";import{d as s,r as o,w as l,c as n,a as c,u as i,o as u,H as f,e as _}from"./index.9fbb40e9.js";const m={class:"container p-3 mx-auto"},v=s({__name:"MdServerFileView",setup(p){const r=o("");return l(()=>{var e,a;r.value=(a=(e=_.currentRoute.value.params)==null?void 0:e.file.toString())!=null?a:""}),(e,a)=>(u(),n("div",m,[c(t,{"file-url":`/server/${r.value}.md`,hljs:i(f)},null,8,["file-url","hljs"])]))}});export{v as default}; 2 | -------------------------------------------------------------------------------- /ui/src/packages/quidjs/errors.ts: -------------------------------------------------------------------------------- 1 | class QuidRequestError extends Error { 2 | hasToLogin: boolean; 3 | response: Response = new Response(null); 4 | 5 | constructor(message: string, response?: Response, hasToLogin?: boolean) { 6 | super(message); 7 | this.name = "QuidError"; 8 | this.stack = (new Error() as any).stack; 9 | this.hasToLogin = hasToLogin ?? false; 10 | if (response) { 11 | this.response = response; 12 | } 13 | } 14 | } 15 | 16 | export { QuidRequestError }; -------------------------------------------------------------------------------- /docs/assets/MdExampleFileView.236940a8.js: -------------------------------------------------------------------------------- 1 | import{_ as s}from"./RenderMdFile.a0539a70.js";import{d as r,r as l,w as o,c as n,a as c,u as i,o as u,H as f,e as m}from"./index.9fbb40e9.js";const _={class:"container p-3 mx-auto"},x=r({__name:"MdExampleFileView",setup(p){const t=l("");return o(()=>{var e,a;t.value=(a=(e=m.currentRoute.value.params)==null?void 0:e.file.toString())!=null?a:""}),(e,a)=>(u(),n("div",_,[c(s,{"file-url":`/examples/${t.value}.md`,hljs:i(f)},null,8,["file-url","hljs"])]))}});export{x as default}; 2 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Quid 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docsite/src/components/TheSidebar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /docker-entrypoint-initdb.d/init-user-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to create user, database, permission on first run only. 4 | # doc: https://github.com/docker-library/docs/blob/master/postgres/README.md#initialization-scripts 5 | 6 | # user pguser already exist => skip errors 7 | 8 | # TODO: replace PASSWORD 'myDBpwd' by "$POSTGRES_DB" 9 | 10 | psql -v --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-END 11 | CREATE USER pguser WITH PASSWORD 'myDBpwd'; 12 | CREATE DATABASE quid; 13 | GRANT ALL PRIVILEGES ON DATABASE quid TO pguser; 14 | END 15 | -------------------------------------------------------------------------------- /docs/assets/ExamplesView.7e99d2a0.js: -------------------------------------------------------------------------------- 1 | import{d as n,o as e,c as s,F as c,i as _,u as l,s as p,t as i,a as m,b as u}from"./index.9fbb40e9.js";const d={class:"flex flex-col space-y-2"},x=["onClick"],f=n({__name:"ExamplesList",setup(o){return(t,r)=>(e(),s("div",d,[(e(!0),s(c,null,_(l(p).examples,a=>(e(),s("div",{onClick:v=>t.$router.push(`/example/${encodeURIComponent(a)}/`),class:"cursor-pointer"},i(a),9,x))),256))]))}}),h={class:"p-3"},$=u("div",{class:"pb-3 text-xl"},"Examples",-1),C=n({__name:"ExamplesView",setup(o){return(t,r)=>(e(),s("div",h,[$,m(f)]))}});export{C as default}; 2 | -------------------------------------------------------------------------------- /ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue'; 3 | import router from "./router"; 4 | import PrimeVue from 'primevue/config'; 5 | import ConfirmationService from 'primevue/confirmationservice'; 6 | import 'primevue/resources/themes/tailwind-light/theme.css' 7 | import 'primevue/resources/primevue.min.css' 8 | import 'primeicons/primeicons.css' 9 | import './assets/index.css'; 10 | import ToastService from 'primevue/toastservice'; 11 | 12 | createApp(App).use(router).use(PrimeVue).use(ToastService).use(ConfirmationService).mount('#app') 13 | -------------------------------------------------------------------------------- /docsite/src/views/MdApiFileView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /ui/src/conf.ts: -------------------------------------------------------------------------------- 1 | import { EnvType, getEnv } from "./env"; 2 | 3 | const env = getEnv(); 4 | 5 | function getServerUrl() { 6 | let url = ""; 7 | if (env == EnvType.local) { 8 | url = "http://localhost:8090"; 9 | } 10 | /*if (process.env.VUE_APP_SERVER_URL !== undefined) { 11 | return process.env.VUE_APP_SERVER_URL; 12 | }*/ 13 | return url; 14 | } 15 | 16 | const conf = { 17 | env: env, 18 | quidUrl: getServerUrl(), 19 | serverUri: getServerUrl(), 20 | isProduction: import.meta.env.PROD 21 | } 22 | 23 | export default conf; -------------------------------------------------------------------------------- /docsite/src/views/MdClientFileView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /docsite/src/views/MdExampleFileView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /docsite/src/views/MdServerFileView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /crypt/crypt_test.go: -------------------------------------------------------------------------------- 1 | package crypt_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/teal-finance/quid/crypt" 7 | ) 8 | 9 | func TestAesGcm(t *testing.T) { 10 | crypt.EncodingKey = []byte("eb037d66a3d07cc90c393a9bb04c172c") 11 | 12 | data := "some plaintext" 13 | out, err := crypt.AesGcmEncryptHex(data) 14 | if err != nil { 15 | t.Fatalf("encryption failed: %v", err) 16 | } 17 | 18 | in, err := crypt.AesGcmDecryptHex(out) 19 | if err != nil { 20 | t.Fatalf("decryption failed: %v", err) 21 | } 22 | 23 | if data != in { 24 | t.Fatalf("expect %x got %x", data, in) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/components/widgets/cards/SimpleCard.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/components/widgets/ActionButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /ui/tests/src/admin/group.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { create_group, delete_group } from './feat/group'; 3 | import { testNs } from "../../conf"; 4 | 5 | test('group', async ({ page }) => { 6 | await page.goto('/group'); 7 | await page.waitForLoadState('domcontentloaded'); 8 | // create 9 | await page.click(`text="${testNs}"`) 10 | const name = 'lambdatestgroup'; 11 | await create_group(page, name) 12 | const row = page.locator('tr', { has: page.locator(`text="${name}"`) }) 13 | await expect(row.locator('td.col-name')).toContainText(name) 14 | // delete 15 | await delete_group(page, name) 16 | //await page.pause(); 17 | }); -------------------------------------------------------------------------------- /docsite/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /ui/tests/playwright.init.config.js: -------------------------------------------------------------------------------- 1 | const { devices } = require('@playwright/test');/** @type {import('@playwright/test').PlaywrightTestConfig} */ 2 | const config = { 3 | workers: 1, 4 | retries: 0, 5 | ignoreHTTPSErrors: true, 6 | use: { 7 | baseURL: 'http://localhost:8090', 8 | headless: false, 9 | viewport: { width: 1280, height: 720 }, 10 | launchOptions: { 11 | slowMo: 100, 12 | }, 13 | }, 14 | projects: [ 15 | { 16 | name: 'chromium', 17 | use: { ...devices['Desktop Chrome'] }, 18 | }, 19 | { 20 | name: 'firefox', 21 | use: { ...devices['Desktop Firefox'] }, 22 | }, 23 | ], 24 | }; 25 | 26 | module.exports = config; -------------------------------------------------------------------------------- /ui/src/packages/restmix/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** The composable parameters */ 2 | interface UseApiParams { 3 | serverUrl?: string; 4 | csrfCookieName?: string; 5 | csrfHeaderKey?: string; 6 | credentials?: RequestCredentials | null; 7 | mode?: RequestMode; 8 | } 9 | 10 | /** The standard api response with typed data */ 11 | interface ApiResponse | Array> { 12 | ok: boolean; 13 | url: string; 14 | headers: Record; 15 | status: number; 16 | statusText: string; 17 | data: T; 18 | text: string; 19 | } 20 | 21 | type OnResponseHook = (res: ApiResponse) => Promise>; 22 | 23 | export { UseApiParams, ApiResponse, OnResponseHook } -------------------------------------------------------------------------------- /ui/src/env.ts: -------------------------------------------------------------------------------- 1 | // The current runtime environement types 2 | enum EnvType { 3 | testRun = "testRun", 4 | local = "local", 5 | production = "production" 6 | } 7 | 8 | // Get the current runtime environement type 9 | function getEnv(): string { 10 | switch (import.meta.env.MODE) { 11 | case "test": 12 | return EnvType.testRun; 13 | case "development": 14 | return EnvType.local; 15 | default: 16 | switch (window.location.hostname) { 17 | case "localhost": 18 | // if running localy without node 19 | return EnvType.local; 20 | default: 21 | return EnvType.production; 22 | } 23 | } 24 | } 25 | 26 | export { EnvType, getEnv }; -------------------------------------------------------------------------------- /ui/tests/init/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { adminUser } from "../conf"; 3 | 4 | test('login', async ({ page, isMobile }) => { 5 | await page.goto("/"); 6 | await page.locator('[placeholder="namespace"]').fill("quid"); 7 | await page.locator('[placeholder="username"]').fill(adminUser.name); 8 | await page.locator('[placeholder="password"]').fill(adminUser.pwd); 9 | await page.locator('text=Submit').click(); 10 | 11 | await page.context().storageState({ path: process.cwd() + '/tests/storage.state.json' }); 12 | console.log("LOGIN COOKIES", await page.context().cookies()) 13 | await page.waitForSelector('text=Quid') 14 | //await page.pause(); 15 | }); 16 | -------------------------------------------------------------------------------- /docsite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "noImplicitAny": false, 12 | "allowJs": true, 13 | "lib": [ 14 | "esnext", 15 | "dom" 16 | ], 17 | "types": [ 18 | "vite/client" 19 | ], 20 | "baseUrl": ".", 21 | "paths": { 22 | "@/*": [ 23 | "src/*" 24 | ] 25 | } 26 | }, 27 | "include": [ 28 | "src/**/*.ts", 29 | "src/**/*.d.ts", 30 | "src/**/*.tsx", 31 | "src/**/*.vue" 32 | ] 33 | } -------------------------------------------------------------------------------- /ui/tests/src/admin/user.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { create_user, delete_user } from './feat/user'; 3 | import { testNs } from "../../conf"; 4 | 5 | test('user', async ({ page }) => { 6 | await page.goto('/user'); 7 | await page.waitForLoadState('domcontentloaded'); 8 | // create 9 | await page.click(`text="${testNs}"`) 10 | const name = 'lambdatestuser'; 11 | const pwd = 'lambdatestuserpwd'; 12 | await create_user(page, name, pwd) 13 | const row = page.locator('tr', { has: page.locator(`text="${name}"`) }) 14 | await expect(row.locator('td.col-name')).toContainText(name) 15 | // delete 16 | await delete_user(page, name) 17 | //await page.pause(); 18 | }); -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "allowJs": true, 12 | "lib": [ 13 | "esnext", 14 | "dom" 15 | ], 16 | "types": [ 17 | "vite/client", 18 | "@types/js-cookie", 19 | "node" 20 | ], 21 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": [ 24 | "src/*" 25 | ] 26 | } 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "src/**/*.d.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue" 33 | ] 34 | } -------------------------------------------------------------------------------- /ui/tests/playwright.config.js: -------------------------------------------------------------------------------- 1 | const { devices } = require('@playwright/test');/** @type {import('@playwright/test').PlaywrightTestConfig} */ 2 | const config = { 3 | workers: 1, 4 | retries: 1, 5 | //globalSetup: require.resolve('./global-setup'), 6 | ignoreHTTPSErrors: true, 7 | use: { 8 | baseURL: 'http://localhost:8090', 9 | headless: true, 10 | viewport: { width: 1280, height: 720 }, 11 | storageState: './tests/storage.state.json', 12 | }, 13 | projects: [ 14 | { 15 | name: 'chromium', 16 | use: { ...devices['Desktop Chrome'] }, 17 | }, 18 | { 19 | name: 'firefox', 20 | use: { ...devices['Desktop Firefox'] }, 21 | }, 22 | ], 23 | }; 24 | 25 | module.exports = config; -------------------------------------------------------------------------------- /ui/tests/src/admin/feat/group.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | async function create_group(page: Page, name: string) { 4 | await page.waitForLoadState('domcontentloaded'); 5 | await page.click('#add-group') 6 | await page.fill('input[type="text"]', name) 7 | await page.click('text=Save') 8 | await page.waitForLoadState('networkidle'); 9 | } 10 | 11 | async function delete_group(page: Page, name: string) { 12 | await page.waitForLoadState('domcontentloaded'); 13 | const row = page.locator('tr', { has: page.locator(`text="${name}"`) }) 14 | await row.locator('td.col-actions > button.delete').click() 15 | await page.locator('.p-confirm-dialog-accept').click() 16 | } 17 | 18 | export { create_group, delete_group } -------------------------------------------------------------------------------- /ui/tests/src/admin/org.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('org', async ({ page, isMobile }) => { 4 | await page.goto('/org'); 5 | await page.waitForLoadState('domcontentloaded'); 6 | // create an org 7 | await page.click('#add-org') 8 | const name = 'lambdatestorg'; 9 | await page.fill('input[type="text"]', name) 10 | await page.click('text=Save') 11 | await page.waitForLoadState('networkidle'); 12 | const row = page.locator('tr', { has: page.locator(`text="${name}"`) }) 13 | await expect(row.locator('td.col-name')).toContainText(name) 14 | // delete the org 15 | await row.locator('td.col-actions > button.delete').click() 16 | await page.locator('.p-confirm-dialog-accept').click() 17 | //await page.pause(); 18 | }); -------------------------------------------------------------------------------- /ui/src/const/categories.ts: -------------------------------------------------------------------------------- 1 | import Category from "@/models/category"; 2 | 3 | const serverAdminCategories = new Set([ 4 | new Category({ title: "Namespaces", icon: "eos-icons:namespace", url: "/namespaces", type: "serverAdmin" }), 5 | new Category({ title: "Admins", icon: "eos-icons:admin-outlined", url: "/admins", type: "serverAdmin" }), 6 | new Category({ title: "Organisations", icon: "fluent:organization-12-regular", url: "/org", type: "serverAdmin" }) 7 | ]); 8 | 9 | const nsAdminCategories = new Set([ 10 | new Category({ title: "Groups", icon: "clarity:group-solid", url: "/group", type: "nsAdmin" }), 11 | new Category({ title: "Users", icon: "clarity:user-solid", url: "/user", type: "nsAdmin" }), 12 | ]); 13 | 14 | export { serverAdminCategories, nsAdminCategories }; -------------------------------------------------------------------------------- /docsite/src/conf.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js/lib/core'; 2 | import typescript from 'highlight.js/lib/languages/typescript'; 3 | import javascript from 'highlight.js/lib/languages/javascript'; 4 | import go from 'highlight.js/lib/languages/go'; 5 | import python from 'highlight.js/lib/languages/python'; 6 | import bash from 'highlight.js/lib/languages/bash'; 7 | hljs.registerLanguage('typescript', typescript); 8 | hljs.registerLanguage('javascript', javascript); 9 | hljs.registerLanguage('go', go); 10 | hljs.registerLanguage('bash', bash); 11 | hljs.registerLanguage('python', python); 12 | 13 | const libName = "Quid"; 14 | 15 | const links: Array<{ href: string; name: string }> = [ 16 | 17 | ]; 18 | 19 | const examplesExtension = ".ts"; 20 | 21 | export { libName, links, examplesExtension, hljs } -------------------------------------------------------------------------------- /ui/src/components/namespace/TheCurrentNamespace.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /docs/server/go.md: -------------------------------------------------------------------------------- 1 | ## Go 2 | 3 | Example server: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "net/http" 10 | "os" 11 | 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo/v4/middleware" 14 | ) 15 | 16 | var key = os.Getenv("QUID_DEMO_KEY") 17 | 18 | func main() { 19 | e := echo.New() 20 | e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 21 | AllowOrigins: []string{"*"}, 22 | AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, 23 | })) 24 | e.Use(middleware.Logger()) 25 | e.Use(middleware.JWTWithConfig(middleware.JWTConfig{ 26 | SigningKey: []byte(key), 27 | })) 28 | e.GET("/", func(c echo.Context) error { 29 | return c.String(http.StatusOK, "ok") 30 | }) 31 | e.Logger.Fatal(e.Start("127.0.0.1:5000")) 32 | } 33 | ``` -------------------------------------------------------------------------------- /docsite/public/server/go.md: -------------------------------------------------------------------------------- 1 | ## Go 2 | 3 | Example server: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "net/http" 10 | "os" 11 | 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo/v4/middleware" 14 | ) 15 | 16 | var key = os.Getenv("QUID_DEMO_KEY") 17 | 18 | func main() { 19 | e := echo.New() 20 | e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 21 | AllowOrigins: []string{"*"}, 22 | AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, 23 | })) 24 | e.Use(middleware.Logger()) 25 | e.Use(middleware.JWTWithConfig(middleware.JWTConfig{ 26 | SigningKey: []byte(key), 27 | })) 28 | e.GET("/", func(c echo.Context) error { 29 | return c.String(http.StatusOK, "ok") 30 | }) 31 | e.Logger.Fatal(e.Start("127.0.0.1:5000")) 32 | } 33 | ``` -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # This file teaches `git log` and friends the canonical names 2 | # and email addresses to use for our contributors. 3 | # 4 | # For details on the format, see: 5 | # https://git-scm.com/docs/gitmailmap 6 | # https://git.github.io/htmldocs/gitmailmap.html 7 | # 8 | # Handy commands for examining or adding to this file: 9 | # 10 | # git shortlog -esn 11 | # 12 | # git shortlog -es | sort -k2 13 | # 14 | # git log --format='%an <%ae>' --author=$NAME | sort | uniq -c 15 | # 16 | # git log --format='%an <%ae>' --author=$NAME | sort -u 17 | 18 | 0uep <0uep@hessling.fr> 19 | kahlys 20 | micheartin 21 | O. Libre 22 | synw 23 | synw 24 | synw 25 | synw 26 | -------------------------------------------------------------------------------- /ui/src/models/siteuser/index.ts: -------------------------------------------------------------------------------- 1 | import { User as SwUser } from "@snowind/state"; 2 | import { ref } from "@vue/reactivity"; 3 | import { useStorage } from "@vueuse/core"; 4 | import Namespace from "../namespace"; 5 | import NamespaceTable from "../namespace/interface"; 6 | import { UserType } from "./types"; 7 | 8 | export default class SiteUser extends SwUser { 9 | devRefreshToken: string | null = null; 10 | type = ref("nsAdmin"); 11 | namespace = useStorage("namespace", Namespace.empty().toTableRow()); 12 | adminUrl = "/ns"; 13 | 14 | get mustSelectNamespace(): boolean { 15 | return this.namespace.value.id == 0 16 | //&& user.type == "serverAdmin"; 17 | } 18 | 19 | changeNs(nst: NamespaceTable) { 20 | this.namespace.value = nst; 21 | } 22 | 23 | resetNs() { 24 | this.namespace.value = Namespace.empty().toTableRow(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docsite/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | 3 | module.exports = { 4 | content: [ 5 | './index.html', 6 | './src/**/*.{js,jsx,ts,tsx,vue}', 7 | './node_modules/@snowind/**/*.{vue,js,ts}', 8 | './node_modules/vuepython/**/*.{vue,js,ts}', 9 | ], 10 | darkMode: 'class', 11 | plugins: [ 12 | require('@tailwindcss/forms'), 13 | require('@snowind/plugin'), 14 | require('tailwindcss-semantic-colors'), 15 | require('@tailwindcss/typography'), 16 | ], 17 | theme: { 18 | extend: { 19 | semanticColors: { 20 | secondary: { 21 | light: { 22 | bg: colors.cyan[500], 23 | txt: colors.white 24 | }, 25 | dark: { 26 | bg: colors.slate[900], 27 | txt: colors.neutral[100] 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /ui/src/components/namespace/NamespaceInfo.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /docsite/src/widgets/RenderMdFile.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /ui/tests/playwright.dev.config.js: -------------------------------------------------------------------------------- 1 | const { devices } = require('@playwright/test');/** @type {import('@playwright/test').PlaywrightTestConfig} */ 2 | const config = { 3 | workers: 1, 4 | retries: 0, 5 | //globalSetup: require.resolve('./global-setup'), 6 | ignoreHTTPSErrors: true, 7 | use: { 8 | baseURL: 'http://localhost:8090', 9 | headless: false, 10 | viewport: { width: 1280, height: 720 }, 11 | storageState: './tests/storage.state.json', 12 | launchOptions: { 13 | slowMo: 1500, 14 | }, 15 | }, 16 | projects: [ 17 | { 18 | name: 'chromium', 19 | use: { ...devices['Desktop Chrome'] }, 20 | }, 21 | { 22 | name: 'firefox', 23 | use: { ...devices['Desktop Firefox'] }, 24 | }, 25 | // Test against mobile viewports. 26 | { 27 | name: 'safari', 28 | use: { ...devices['iPhone 12'] }, 29 | }, 30 | ], 31 | }; 32 | 33 | module.exports = config; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config.json 2 | /go.work* 3 | /quid 4 | *_ORI 5 | *_ORI.go 6 | *.go.old 7 | 8 | # Required for Heroku deployment 9 | /ui/dist 10 | 11 | /ui/node_modules 12 | /ui/yarn.lock 13 | /ui/tests/storage.state.json 14 | /tests/storage.state.json 15 | 16 | .DS_Store 17 | /ui/.yarnrc 18 | *.local 19 | /dev 20 | /ui/storybook-static 21 | /ui/yarn-error.log 22 | /ui/bin 23 | package-lock.json 24 | 25 | # local env files 26 | /ui/.env.local 27 | /ui/.env.*.local 28 | 29 | # Log files 30 | /ui/npm-debug.log* 31 | /ui/yarn-debug.log* 32 | /ui/yarn-error.log* 33 | /ui/pnpm-debug.log* 34 | 35 | # Folders 36 | _obj 37 | _test 38 | .project 39 | .metadata 40 | 41 | *.cgo1.go 42 | *.cgo2.c 43 | _cgo_defun.c 44 | _cgo_gotypes.go 45 | _cgo_export.* 46 | 47 | _testmain.go 48 | 49 | *.exe 50 | *.test 51 | *.prof 52 | 53 | # Editor directories and files 54 | /.idea 55 | /.vscode 56 | *.suo 57 | *.ntvs* 58 | *.njsproj 59 | *.sln 60 | *.sw? 61 | 62 | /code-coverage.out 63 | -------------------------------------------------------------------------------- /server/db/errors.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lib/pq" 7 | ) 8 | 9 | // QueryResult :. 10 | type QueryResult struct { 11 | Error QueryError 12 | HasError bool 13 | } 14 | 15 | // QueryError :. 16 | type QueryError struct { 17 | Message string 18 | HasUserMessage bool 19 | } 20 | 21 | func (e *QueryError) Error() string { 22 | return fmt.Sprintf("%t:%v: query error", e.HasUserMessage, e.Message) 23 | } 24 | 25 | func queryNoError() QueryResult { 26 | return QueryResult{HasError: false} 27 | } 28 | 29 | func queryError(err error) QueryResult { 30 | e, isPq := err.(*pq.Error) 31 | if isPq { 32 | return QueryResult{ 33 | HasError: true, 34 | Error: QueryError{ 35 | Message: e.Message, 36 | HasUserMessage: true, 37 | }, 38 | } 39 | } 40 | 41 | return QueryResult{ 42 | HasError: true, 43 | Error: QueryError{ 44 | Message: e.Error(), 45 | HasUserMessage: false, 46 | }, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ui/tests/src/admin/feat/user.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | async function create_user(page: Page, name: string, pwd: string) { 4 | await page.waitForLoadState('domcontentloaded'); 5 | await page.click('#add-user') 6 | await page.fill('input[type="text"]', name) 7 | await page.fill('input[type="password"] >> nth=0', pwd) 8 | await page.fill('input[type="password"] >> nth=1', pwd) 9 | await page.click('text=Save') 10 | await page.waitForLoadState('networkidle'); 11 | } 12 | 13 | async function delete_user(page: Page, name: string) { 14 | await page.waitForLoadState('domcontentloaded'); 15 | const row = page.locator('tr', { has: page.locator(`text="${name}"`) }) 16 | await row.locator('td.col-actions > button.delete').click() 17 | await page.waitForSelector('.p-confirm-dialog-accept') 18 | await page.locator('.p-confirm-dialog-accept').click() 19 | await page.waitForLoadState('networkidle'); 20 | } 21 | 22 | export { create_user, delete_user } -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: debian-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v2 21 | -------------------------------------------------------------------------------- /docsite/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/vue-next/pull/3399 4 | 5 | declare module 'vue' { 6 | export interface GlobalComponents { 7 | ApidocsList: typeof import('./src/components/ApidocsList.vue')['default'] 8 | ClientList: typeof import('./src/components/ClientList.vue')['default'] 9 | ExamplesList: typeof import('./src/components/ExamplesList.vue')['default'] 10 | 'IEva:arrowBackOutline': typeof import('~icons/eva/arrow-back-outline')['default'] 11 | 'IFaSolid:moon': typeof import('~icons/fa-solid/moon')['default'] 12 | 'IFaSolid:sun': typeof import('~icons/fa-solid/sun')['default'] 13 | ServerList: typeof import('./src/components/ServerList.vue')['default'] 14 | TheHeader: typeof import('./src/components/TheHeader.vue')['default'] 15 | TheSidebar: typeof import('./src/components/TheSidebar.vue')['default'] 16 | } 17 | } 18 | 19 | export { } 20 | -------------------------------------------------------------------------------- /ui/src/models/org/org.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/api"; 2 | import OrgContract from "./contract"; 3 | import { OrgTable } from "./interface"; 4 | 5 | export default class Org { 6 | id: number; 7 | name: string; 8 | 9 | constructor(data: OrgContract) { 10 | this.id = data.id; 11 | this.name = data.name; 12 | } 13 | 14 | toTableRow(): OrgTable { 15 | const row = Object.create(this); 16 | row.actions = []; 17 | return row as OrgTable; 18 | } 19 | 20 | static async fetchAll(): Promise> { 21 | const url = "/admin/orgs/all"; 22 | const data = new Array(); 23 | try { 24 | const resp = await api.get>(url); 25 | resp.data.forEach((row) => data.push(new Org(row).toTableRow())); 26 | } catch (e) { 27 | console.log("Err", e); 28 | throw e; 29 | } 30 | return data; 31 | } 32 | 33 | static async delete(id: number) { 34 | await api.post("/admin/orgs/delete", { 35 | id: id, 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /ui/src/interface.ts: -------------------------------------------------------------------------------- 1 | import { ConfirmationOptions } from "primevue/confirmationoptions"; 2 | 3 | interface ConfirmOptions { 4 | require: (option: ConfirmationOptions) => void; 5 | close: () => void; 6 | } 7 | 8 | interface UserStatusContract { 9 | admin_type: "NsAdmin" | "QuidAdmin"; 10 | username: string; 11 | ns: { 12 | id: number; 13 | name: string; 14 | } 15 | } 16 | 17 | interface NotifyService { 18 | error: (content: string) => void; 19 | warning: (title: string, content: string, timeOnScreen?: number) => void; 20 | success: (title: string, content: string, timeOnScreen?: number) => void; 21 | done: (content: string) => void; 22 | confirmDelete: (msg: string, onConfirm: CallableFunction, onReject?: CallableFunction, title?: string) => void; 23 | } 24 | 25 | type AlgoType = "HMAC" | "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512" | "ES256" | "ES384" | "ES512" | "EDDSA"; 26 | 27 | export { ConfirmOptions, NotifyService, UserStatusContract, AlgoType } 28 | -------------------------------------------------------------------------------- /ui/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: Array = [ 4 | { 5 | path: '/', 6 | name: 'Home', 7 | component: () => import('./views/HomeView.vue') 8 | }, 9 | { 10 | path: '/namespaces', 11 | name: 'Namespaces', 12 | component: () => import('./views/NamespaceView.vue') 13 | }, 14 | { 15 | path: '/admins', 16 | name: 'Admins', 17 | component: () => import('./views/AdminsView.vue') 18 | }, 19 | { 20 | path: '/group', 21 | name: 'Groups', 22 | component: () => import('./views/GroupView.vue') 23 | }, 24 | { 25 | path: '/user', 26 | name: 'Users', 27 | component: () => import('./views/UserView.vue') 28 | }, 29 | { 30 | path: '/org', 31 | name: 'Orgs', 32 | component: () => import('./views/OrgView.vue') 33 | }, 34 | ] 35 | 36 | const router = createRouter({ 37 | history: createWebHistory(import.meta.env.BASE_URL), 38 | routes 39 | }) 40 | 41 | export default router 42 | -------------------------------------------------------------------------------- /ui/tests/README.md: -------------------------------------------------------------------------------- 1 | # End to end tests 2 | 3 | ## Install 4 | 5 | ```bash 6 | cd ui 7 | yarn 8 | # install the playwright stuff 9 | npx playwright install 10 | ``` 11 | 12 | ## Initialize 13 | 14 | Create some test data in the database: 15 | 16 | - Create in the `quid` namespace a user named `admin` with a password `adminpwd` 17 | - Create a namepace named `testns` 18 | 19 | Run this to get an initial test config to auto login the user before each test: 20 | 21 | ```bash 22 | yarn testinit 23 | ``` 24 | 25 | ## List tests 26 | 27 | Show the available tests and playbooks: 28 | 29 | ```bash 30 | yarn showtests 31 | ``` 32 | 33 | ## Run tests headless 34 | 35 | Run all the available tests headless: 36 | 37 | ```bash 38 | yarn runtest 39 | ``` 40 | 41 | Run the admin tests headless: 42 | 43 | ```bash 44 | yarn runtest playbook=admin 45 | ``` 46 | 47 | ## Run test in browser 48 | 49 | Run a test in dev mode in a Firefox browser: 50 | 51 | ```bash 52 | yarn fftest test=admin/namespace 53 | ``` 54 | 55 | Run a test in dev mode in a Chromium browser: 56 | 57 | ```bash 58 | yarn crtest test=admin/namespace 59 | ``` -------------------------------------------------------------------------------- /ui/src/components/widgets/cards/CardsGrid.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /docsite/src/widgets/RenderMd.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 41 | 42 | -------------------------------------------------------------------------------- /docsite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import typescript2 from "rollup-plugin-typescript2" 4 | import vue from '@vitejs/plugin-vue' 5 | import Components from 'unplugin-vue-components/vite' 6 | import Icons from 'unplugin-icons/vite' 7 | import IconsResolver from 'unplugin-icons/resolver' 8 | import { libName } from "./src/conf"; 9 | 10 | export default defineConfig({ 11 | plugins: [ 12 | typescript2({ 13 | check: false, 14 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 15 | clean: true 16 | }), 17 | vue(), 18 | Components({ 19 | resolvers: [ 20 | IconsResolver() 21 | ], 22 | }), 23 | Icons({ 24 | scale: 1.2, 25 | defaultClass: 'inline-block align-middle', 26 | compiler: 'vue3', 27 | }), 28 | ], 29 | base: process.env.NODE_ENV === 'production' ? `/${libName.toLowerCase()}/` : './', 30 | resolve: { 31 | alias: [ 32 | { find: '@/', replacement: '/src/' }, 33 | { 34 | find: 'vue', 35 | replacement: path.resolve("./node_modules/vue"), 36 | }, 37 | ] 38 | }, 39 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Teal.Finance/Quid contributors 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 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | 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 | -------------------------------------------------------------------------------- /docsite/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 synw 4 | Copyright (c) 2022 Teal.Finance/Quid contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /ui/src/models/user/user.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/api"; 2 | import UserContract from "./contract"; 3 | import { UserTable } from "./interface"; 4 | import { user } from "@/state"; 5 | 6 | export default class User { 7 | id: number; 8 | name: string; 9 | 10 | constructor(user: UserContract) { 11 | this.id = user.id; 12 | this.name = user.name; 13 | } 14 | 15 | toTableRow(): UserTable { 16 | const row = Object.create(this); 17 | row.actions = []; 18 | return row as UserTable; 19 | } 20 | 21 | static async fetchAll(nsid: number): Promise> { 22 | const url = user.adminUrl + "/users/nsall"; 23 | const data = new Array(); 24 | try { 25 | const payload = { ns_id: nsid } 26 | const resp = await api.post>(url, payload); 27 | resp.data.forEach((row) => data.push(new User(row).toTableRow())); 28 | } catch (e) { 29 | console.log("Err", e); 30 | throw e; 31 | } 32 | return data; 33 | } 34 | 35 | static async delete(id: number) { 36 | const url = user.adminUrl + "/users/delete"; 37 | await api.post(url, { 38 | id: id, 39 | ns_id: user.namespace.value.id 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docsite/src/state.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@snowind/state"; 2 | import { useApi } from '@snowind/api'; 3 | import { libName } from "./conf"; 4 | import { reactive } from "vue"; 5 | 6 | const user = new User(); 7 | const api = useApi({ serverUrl: import.meta.env.MODE === 'development' ? '' : `/${libName.toLowerCase()}` }); 8 | const state = reactive({ 9 | apidocs: new Array(), 10 | server: new Array(), 11 | client: new Array(), 12 | examples: new Array(), 13 | }) 14 | 15 | function fetchIndexes() { 16 | api.get>("/apidoc/index.json").then((res) => { 17 | state.apidocs = typeof res == "string" ? JSON.parse(res) : res 18 | }); 19 | api.get>("/server/index.json").then(res => 20 | state.server = typeof res == "string" ? JSON.parse(res) : res 21 | ); 22 | api.get>("/client/index.json").then((res) => { 23 | state.client = typeof res == "string" ? JSON.parse(res) : res 24 | }); 25 | api.get>("/examples/index.json").then(res => 26 | state.examples = typeof res == "string" ? JSON.parse(res) : res 27 | ); 28 | } 29 | 30 | function initState() { 31 | fetchIndexes() 32 | } 33 | 34 | export { user, api, state, initState } -------------------------------------------------------------------------------- /tokens/claims.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "time" 5 | 6 | jwt "github.com/golang-jwt/jwt/v4" 7 | ) 8 | 9 | // AccessClaims is the standard claims for a user access token. 10 | type AccessClaims struct { 11 | Username string `json:"usr,omitempty"` 12 | Groups []string `json:"grp,omitempty"` 13 | Orgs []string `json:"org,omitempty"` 14 | jwt.RegisteredClaims 15 | } 16 | 17 | // RefreshClaims is the standard claims for a user refresh token. 18 | type RefreshClaims struct { 19 | Namespace string `json:"namespace,omitempty"` 20 | Username string `json:"username,omitempty"` 21 | jwt.RegisteredClaims 22 | } 23 | 24 | // newAccessClaims creates a standard claim for a user access token. 25 | func newAccessClaims(username string, groups, orgs []string, expiry time.Time) AccessClaims { 26 | return AccessClaims{ 27 | username, 28 | groups, 29 | orgs, 30 | jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(expiry)}, 31 | } 32 | } 33 | 34 | // newRefreshClaims creates a standard claim for a user refresh token. 35 | func newRefreshClaims(namespace, user string, expiry time.Time) RefreshClaims { 36 | return RefreshClaims{ 37 | namespace, 38 | user, 39 | jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(expiry)}, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors') 2 | 3 | module.exports = { 4 | content: [ 5 | './index.html', 6 | './src/**/*.{js,jsx,ts,tsx,vue}', 7 | './node_modules/@snowind/**/*.{vue,js,ts}', 8 | ], 9 | darkMode: 'class', 10 | plugins: [ 11 | require('@tailwindcss/forms'), 12 | require('@snowind/plugin'), 13 | require('tailwindcss-semantic-colors'), 14 | ], 15 | theme: { 16 | extend: { 17 | semanticColors: { 18 | primary: { 19 | light: { 20 | bg: colors.cyan[800], 21 | txt: colors.white 22 | }, 23 | dark: { 24 | bg: '#114B5E', 25 | txt: colors.neutral[100] 26 | } 27 | }, 28 | topbar: { 29 | light: { 30 | bg: colors.cyan[800], 31 | txt: colors.white 32 | }, 33 | dark: { 34 | bg: '#080807', 35 | txt: colors.neutral[400] 36 | } 37 | }, 38 | sidebar: { 39 | light: { 40 | bg: colors.cyan[500], 41 | txt: colors.white 42 | }, 43 | dark: { 44 | bg: '#171814', 45 | txt: colors.neutral[400] 46 | } 47 | }, 48 | } 49 | }, 50 | } 51 | } -------------------------------------------------------------------------------- /ui/src/components/org/OrgDatatable.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /docs/assets/RenderMdFile.d4ae6d4e.css: -------------------------------------------------------------------------------- 1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! 2 | Theme: StackOverflow Dark 3 | Description: Dark theme as used on stackoverflow.com 4 | Author: stackoverflow.com 5 | Maintainer: @Hirse 6 | Website: https://github.com/StackExchange/Stacks 7 | License: MIT 8 | Updated: 2021-05-15 9 | 10 | Updated for @stackoverflow/stacks v0.64.0 11 | Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less 12 | Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less 13 | */.hljs{color:#fff;background:#1c1b1b}.hljs-subst{color:#fff}.hljs-comment{color:#999}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag{color:#88aece}.hljs-attribute{color:#c59bc1}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type{color:#f08d49}.hljs-selector-class{color:#88aece}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#b5bd68}.hljs-meta,.hljs-selector-pseudo{color:#88aece}.hljs-built_in,.hljs-literal,.hljs-title{color:#f08d49}.hljs-bullet,.hljs-code{color:#ccc}.hljs-meta .hljs-string{color:#b5bd68}.hljs-deletion{color:#de7176}.hljs-addition{color:#76c490}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.md-content{max-width:120ch} 14 | -------------------------------------------------------------------------------- /ui/tests/src/admin/namespace.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('namespace', async ({ page, isMobile }) => { 4 | await page.goto('/namespaces'); 5 | await page.waitForLoadState('domcontentloaded'); 6 | const quidRow = page.locator('tr', { has: page.locator('text="quid"') }) 7 | await quidRow.locator('td.col-actions >> text="Show info"').click() 8 | await expect(page.locator('tr.p-datatable-row-expansion')).toContainText('quid_admin') 9 | await quidRow.locator('td.col-actions >> text="Hide info"').click() 10 | // create a namespace 11 | await page.click('#add-namespace') 12 | const nsName = 'lambdatestns'; 13 | await page.fill('input[type="text"] >> nth=0', nsName) 14 | await page.click('text=Save') 15 | await page.waitForLoadState('networkidle'); 16 | const row = page.locator('tr', { has: page.locator(`text="${nsName}"`) }) 17 | await expect(row.locator('td.col-name')).toContainText(nsName) 18 | await expect(row.locator('td.col-algo')).toContainText('HS256') 19 | await expect(row.locator('td.col-max-access-ttl')).toContainText('20m') 20 | await expect(row.locator('td.col-max-refresh-ttl')).toContainText('24h') 21 | // delete the namespace 22 | await row.locator('td.col-actions > button.delete').click() 23 | await page.locator('.p-confirm-dialog-accept').click() 24 | //await page.pause(); 25 | }); -------------------------------------------------------------------------------- /ui/src/notify.ts: -------------------------------------------------------------------------------- 1 | import { ToastServiceMethods } from "primevue/toastservice"; 2 | import { ConfirmOptions, NotifyService } from "./interface"; 3 | 4 | 5 | const useNotify = function (toast: ToastServiceMethods, confirm: ConfirmOptions): NotifyService { 6 | return { 7 | error: (content: string) => { 8 | toast.add({ severity: 'error', summary: 'Error', detail: content, group: "main" }); 9 | }, 10 | warning: (title: string, content: string, timeOnScreen = 5000) => { 11 | toast.add({ severity: 'error', summary: title, detail: content, life: timeOnScreen, group: "main" }); 12 | }, 13 | success: (title: string, content: string, timeOnScreen = 1500) => { 14 | toast.add({ severity: 'success', summary: title, detail: content, life: timeOnScreen, group: "main" }); 15 | }, 16 | done: (content: string) => { 17 | toast.add({ severity: 'success', summary: 'Done', detail: content, life: 1500, group: "bottom-right" }); 18 | }, 19 | confirmDelete: (msg: string, onConfirm: CallableFunction, onReject: CallableFunction = () => null, title = "Delete") => { 20 | confirm.require({ 21 | message: msg, 22 | header: title, 23 | icon: 'pi pi-info-circle', 24 | acceptClass: 'p-button-danger', 25 | accept: () => onConfirm(), 26 | reject: () => onReject(), 27 | }); 28 | }, 29 | } 30 | } 31 | 32 | export default useNotify; -------------------------------------------------------------------------------- /ui/src/components/namespace/NamespaceSelector.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /ui/tests/src/admin/nsadmin.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { create_user, delete_user } from './feat/user'; 3 | import { testNs } from "../../conf"; 4 | 5 | test('user', async ({ page }) => { 6 | await page.goto('/user'); 7 | await page.waitForLoadState('domcontentloaded'); 8 | // create 9 | await page.click(`text="${testNs}"`) 10 | const name = 'lambdatestuser'; 11 | const pwd = 'lambdatestuserpwd'; 12 | await create_user(page, name, pwd) 13 | await page.waitForLoadState('networkidle'); 14 | // create administrator 15 | await page.locator('#sidebar >> role=link >> nth=1').click() 16 | await page.waitForLoadState('domcontentloaded'); 17 | await page.locator('#add-admin').click() 18 | await page.locator('input[type="text"]').fill(name) 19 | await page.locator('role=button[name="Search"]').click() 20 | await page.locator(`text=${name}`).click() 21 | await page.locator('role=button[name="Save"]').click() 22 | const row = page.locator('tr', { has: page.locator(`text="${name}"`) }) 23 | await expect(row.locator('td.col-name')).toContainText(name) 24 | // delete administrator 25 | await row.locator('td.col-actions > button.delete').click() 26 | await page.locator('.p-confirm-dialog-accept').click() 27 | // delete user 28 | await page.locator('#sidebar >> role=link >> nth=4').click() 29 | await delete_user(page, name) 30 | //await page.pause(); 31 | }); -------------------------------------------------------------------------------- /docsite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docdundee_ts", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build_to_gh": "vite build && rm -rf ../docs/* && mv ./dist/* ../docs && cd ../docs && ln -s index.html 404.html", 8 | "preview": "vite build && vite preview", 9 | "build_docs": "dundee_docs", 10 | "build_examples": "dundee_ex" 11 | }, 12 | "dependencies": { 13 | "@snowind/api": "0.0.8", 14 | "@snowind/header": "^0.0.8", 15 | "@snowind/state": "0.0.3", 16 | "highlight.js": "^11.6.0", 17 | "markdown-it": "^13.0.1", 18 | "vue": "^3.2.31", 19 | "vue-router": "^4.1.5", 20 | "vuecodit": "0.0.2" 21 | }, 22 | "devDependencies": { 23 | "@iconify/json": "^2.1.26", 24 | "@snowind/plugin": "0.4.0", 25 | "@tailwindcss/forms": "^0.5.0", 26 | "@tailwindcss/typography": "^0.5.7", 27 | "@vitejs/plugin-vue": "^2.3.1", 28 | "@vue/compiler-sfc": "^3.2.31", 29 | "autoprefixer": "^10.4.4", 30 | "docdundee": "^0.0.1", 31 | "path": "^0.12.7", 32 | "postcss": "^8.4.12", 33 | "rollup-plugin-typescript2": "^0.31.2", 34 | "sass": "^1.50.0", 35 | "tailwindcss": "^3.0.23", 36 | "tailwindcss-semantic-colors": "^0.2.0", 37 | "tslib": "^2.3.1", 38 | "typescript": "^4.6.3", 39 | "unplugin-icons": "^0.14.1", 40 | "unplugin-vue-components": "^0.18.5", 41 | "vite": "^2.9.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/db/tokens.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/teal-finance/quid/tokens" 5 | ) 6 | 7 | // genNsAdminToken : generate a refresh token for an admin user and namespace 8 | // Deprecated because this function is not used. 9 | func genNsAdminToken(username, nsName string) (string, error) { 10 | log.Info("Generating NS Admin token for", username, nsName) 11 | 12 | // get the namespace 13 | ns, err := SelectNsFromName(nsName) 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | uid, err := selectEnabledUsrID(username) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | // check admin perms 24 | adminType, err := GetUserType(nsName, ns.ID, uid) 25 | if err != nil { 26 | return "", err 27 | } 28 | if adminType == UserNoAdmin { 29 | qid, err := SelectNsID("quid") 30 | if err != nil { 31 | return "", err 32 | } 33 | isAdmin, err := IsUserInAdminGroup(uid, qid) 34 | if err != nil { 35 | return "", err 36 | } 37 | if !isAdmin { 38 | return "", log.Warn("the user is not a namespace admin").Err() 39 | } 40 | } 41 | 42 | log.Encrypt("Gen token", ns.MaxRefreshTTL, ns.MaxRefreshTTL, ns.Name, username, []byte(ns.RefreshKey)) 43 | 44 | token, err := tokens.GenRefreshToken(ns.MaxRefreshTTL, ns.MaxRefreshTTL, ns.Name, username, []byte(ns.RefreshKey)) 45 | if err != nil { 46 | return "", log.Error("Error generating refresh token", err).Err() 47 | } 48 | return token, nil 49 | } 50 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Quid — JWT authentication server — Administration interface 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ui/src/components/org/AddOrg.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 52 | -------------------------------------------------------------------------------- /docsite/src/views/JsExamplesDetailView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/views/OrgView.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 46 | -------------------------------------------------------------------------------- /ui/src/components/group/AddGroup.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | -------------------------------------------------------------------------------- /ui/src/components/group/GroupDatatable.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot that automatically updates dependencies 2 | # Documentation: 3 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 4 | 5 | version: 2 6 | updates: 7 | 8 | # Maintain dependencies for GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | day: "friday" 14 | assignees: 15 | - "olibre" 16 | open-pull-requests-limit: 2 17 | 18 | # Maintain dependencies for npm 19 | - package-ecosystem: "npm" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | day: "friday" 24 | assignees: 25 | - "olibre" 26 | open-pull-requests-limit: 2 27 | 28 | # Maintain dependencies for Composer 29 | - package-ecosystem: "composer" 30 | directory: "/" 31 | schedule: 32 | interval: "weekly" 33 | day: "friday" 34 | assignees: 35 | - "olibre" 36 | open-pull-requests-limit: 2 37 | 38 | # Maintain dependencies for Composer 39 | - package-ecosystem: "docker" 40 | directory: "/" 41 | schedule: 42 | interval: "weekly" 43 | day: "friday" 44 | assignees: 45 | - "olibre" 46 | open-pull-requests-limit: 2 47 | 48 | # Maintain dependencies for Composer 49 | - package-ecosystem: "gomod" 50 | directory: "/" 51 | schedule: 52 | interval: "weekly" 53 | day: "friday" 54 | assignees: 55 | - "olibre" 56 | open-pull-requests-limit: 2 57 | -------------------------------------------------------------------------------- /ui/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Quid — JWT authentication server — Administration interface 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ui/src/state.ts: -------------------------------------------------------------------------------- 1 | import { ToastServiceMethods } from "primevue/toastservice"; 2 | import { api, checkStatus } from "./api"; 3 | import conf from "./conf"; 4 | import { EnvType } from "./env"; 5 | import { ConfirmOptions, NotifyService } from "./interface"; 6 | import SiteUser from "./models/siteuser"; 7 | import useNotify from "./notify"; 8 | import { useScreenSize } from "@snowind/state"; 9 | import Namespace from "./models/namespace"; 10 | 11 | const user = new SiteUser(); 12 | let notify: NotifyService; 13 | const { isMobile, isTablet, isDesktop } = useScreenSize(); 14 | 15 | async function initState(toast: ToastServiceMethods, confirm: ConfirmOptions) { 16 | notify = useNotify(toast, confirm) 17 | await initUserState(); 18 | } 19 | 20 | async function initUserState() { 21 | const { ok, status } = await checkStatus(); 22 | if (!ok) { 23 | console.log("Status unauthorized"); 24 | return 25 | } 26 | console.log("Status", status) 27 | user.isLoggedIn.value = true; 28 | user.name.value = status.username; 29 | const ns = Namespace.empty(); 30 | ns.id = status.ns.id; 31 | ns.name = status.ns.name; 32 | if (status.admin_type === "QuidAdmin") { 33 | user.type.value = "serverAdmin"; 34 | user.adminUrl = "/admin"; 35 | user.resetNs() 36 | } else if (status.admin_type === "NsAdmin") { 37 | user.changeNs(ns.toTableRow()); 38 | } else { 39 | throw new Error(`Unknown admin type ${status.admin_type}`) 40 | } 41 | } 42 | 43 | export { user, initState, initUserState, notify, isMobile, isTablet, isDesktop } 44 | -------------------------------------------------------------------------------- /ui/src/components/admin/AdminDatatable.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | -------------------------------------------------------------------------------- /ui/vite.config.mts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import typescript from '@rollup/plugin-typescript'; 4 | import vue from '@vitejs/plugin-vue' 5 | import Components from 'unplugin-vue-components/vite' 6 | import Icons from 'unplugin-icons/vite' 7 | import IconsResolver from 'unplugin-icons/resolver' 8 | 9 | export default defineConfig({ 10 | build: { 11 | rollupOptions: { 12 | output: { 13 | // The [hash] permits the use of "Cache-Control: max-age=604800" 14 | // because [hash] will be different each time its content changes. 15 | // https://rollupjs.org/guide/en/#outputentryfilenames 16 | // https://rollupjs.org/guide/en/#outputchunkfilenames 17 | // https://rollupjs.org/guide/en/#outputassetfilenames 18 | entryFileNames: `js/[name]-[hash].js`, 19 | chunkFileNames: `js/[name]-[hash].js`, 20 | assetFileNames: `assets/[name]-[hash].[ext]`, 21 | // CSS and images go to assetFileNames folder. 22 | // To put images somewhere else use the public folder: 23 | // https://vitejs.dev/guide/assets.html#the-public-directory 24 | }, 25 | }, 26 | }, 27 | plugins: [ 28 | typescript(), 29 | vue(), 30 | Components({ 31 | resolvers: [ 32 | IconsResolver() 33 | ], 34 | }), 35 | Icons({ 36 | scale: 1.2, 37 | defaultClass: 'inline-block align-middle', 38 | compiler: 'vue3', 39 | }), 40 | ], 41 | resolve: { 42 | alias: [ 43 | { find: '@/', replacement: '/src/' } 44 | ] 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /cmd/quid/conf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "os" 8 | 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | // createConfigFile : createConfigFile a config file. 13 | func createConfigFile(dbName, dbUser, dbPass string) error { 14 | data := map[string]any{ 15 | "db_name": dbName, 16 | "db_user": dbUser, 17 | "db_password": dbPass, 18 | "key": randomAES128KeyHex(), 19 | } 20 | 21 | jsonString, err := json.MarshalIndent(data, "", " ") 22 | if err != nil { 23 | log.Warn(err) 24 | return err 25 | } 26 | 27 | return os.WriteFile("config.json", jsonString, os.ModePerm) 28 | } 29 | 30 | // readConfigFile : get the config. 31 | func readConfigFile() (name, usr, pwd, key string) { 32 | viper.SetConfigName("config") 33 | viper.AddConfigPath(".") 34 | 35 | err := viper.ReadInConfig() 36 | if err != nil { 37 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 38 | log.Info(`No "config.json" file. Note: flag -conf generates a "config.json" file with a random AES-128 key.`) 39 | return "", "", "", "" 40 | } else { 41 | log.Fatal(err) 42 | } 43 | } 44 | 45 | name = viper.Get("db_name").(string) 46 | usr = viper.Get("db_user").(string) 47 | pwd = viper.Get("db_password").(string) 48 | key = viper.Get("key").(string) 49 | 50 | // conn = "dbname=" + name + " user=" + usr + " password=" + pwd + " sslmode=disable" 51 | return name, usr, pwd, key 52 | } 53 | 54 | func randomAES128KeyHex() string { 55 | bytes := make([]byte, 16) 56 | if _, err := rand.Read(bytes); err != nil { 57 | log.Panic(err) 58 | } 59 | return hex.EncodeToString(bytes) 60 | } 61 | -------------------------------------------------------------------------------- /docsite/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router" 2 | import HomeView from "./views/HomeView.vue" 3 | import { libName } from "./conf" 4 | 5 | const baseTitle = libName; 6 | 7 | const routes: Array = [ 8 | { 9 | path: "/", 10 | component: HomeView, 11 | meta: { 12 | title: "Home" 13 | } 14 | }, 15 | { 16 | path: "/apidoc", 17 | component: () => import("./views/ApidocView.vue"), 18 | meta: { 19 | title: "Apidoc" 20 | } 21 | }, 22 | { 23 | path: "/apidoc/:file", 24 | component: () => import("./views/MdApiFileView.vue"), 25 | meta: { 26 | title: "Apidoc" 27 | } 28 | }, 29 | { 30 | path: "/server/:file", 31 | component: () => import("./views/MdServerFileView.vue"), 32 | meta: { 33 | title: "Server" 34 | } 35 | }, 36 | { 37 | path: "/client/:file", 38 | component: () => import("./views/MdClientFileView.vue"), 39 | meta: { 40 | title: "Client" 41 | } 42 | }, 43 | { 44 | path: "/example/:file", 45 | component: () => import("./views/MdExampleFileView.vue"), 46 | meta: { 47 | title: "Example" 48 | } 49 | }, 50 | { 51 | path: "/examples", 52 | component: () => import("./views/ExamplesView.vue"), 53 | meta: { 54 | title: "Examples" 55 | } 56 | } 57 | ] 58 | 59 | const router = createRouter({ 60 | history: createWebHistory(import.meta.env.BASE_URL), 61 | routes 62 | }); 63 | 64 | router.afterEach((to, from) => { // eslint-disable-line 65 | document.title = `${baseTitle} - ${to.meta?.title}` 66 | }); 67 | 68 | export default router -------------------------------------------------------------------------------- /docs/client/js.md: -------------------------------------------------------------------------------- 1 | ## Javascript client 2 | 3 | [![pub package](https://img.shields.io/npm/v/quidjs)](https://www.npmjs.com/package/quidjs) 4 | 5 | Install the [Quidjs library](https://github.com/teal-finance/quidjs). It transparently manage the requests to api 6 | servers. If a server returns a 401 Unauthorized response when an access token is expired the client library will 7 | request a new access token from a Quid server, using a refresh token, and will retry the request with the new 8 | access token 9 | 10 | ```bash 11 | yarn add quidjs 12 | # or 13 | npm install quidjs 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```typescript 19 | import { useQuidRequests } from "quidjs"; 20 | 21 | const quid = useQuidRequests({ 22 | namespace: "my_namespace", 23 | timeouts: { 24 | accessToken: "5m", 25 | refreshToken: "24h" 26 | }, 27 | quidUri: "https://localhost:8082", // quid server url 28 | serverUri: "https://localhost:8000", // url of your backend 29 | verbose: true, 30 | }); 31 | 32 | // login the user 33 | await quid.login("user", "pwd"); 34 | 35 | // use the quid instance to request json from the backend 36 | // GET request 37 | const response: Record = await quid.get>("/api/get"); 38 | console.log("Backend GET response:", response) 39 | // POST request 40 | const payload = {"foo": "bar"}; 41 | const response2: Record = await quid.post>("/api/post", payload); 42 | console.log("Backend POST response:", response2) 43 | ``` 44 | 45 | ## Examples 46 | 47 | - [Script src](https://github.com/teal-finance/quidjs/tree/master/examples/umd) 48 | - [Script module](https://github.com/teal-finance/quidjs/tree/master/examples/esm) -------------------------------------------------------------------------------- /docs/server/python.md: -------------------------------------------------------------------------------- 1 | ## Python 2 | 3 | ### Decode tokens 4 | 5 | ```python 6 | import jwt 7 | 8 | try: 9 | payload = jwt.decode(token, key, algorithms=['HS256']) 10 | except jwt.ExpiredSignatureError: 11 | # ... 12 | ``` 13 | 14 | Payload example: 15 | 16 | ```json 17 | { 18 | "usr": "jane", 19 | "grp": ["group1", "group2"], 20 | "org": ["organization1", "organization2"], 21 | "exp": 1595950745 22 | } 23 | ``` 24 | 25 | Note: `"exp"` is the expiration timestamp in [Unix time](https://en.wikipedia.org/wiki/Unix_time) format (seconds since 1970). 26 | 27 | ### Flask server example 28 | 29 | ```python 30 | from datetime import datetime 31 | from flask import Flask, abort 32 | from flask_cors import CORS, cross_origin 33 | from flask import request 34 | import jwt 35 | import os 36 | 37 | """ 38 | To run: 39 | export FLASK_APP=server.py 40 | flask run 41 | """ 42 | 43 | app = Flask(__name__) 44 | cors = CORS(app) 45 | app.config['CORS_HEADERS'] = 'Content-Type' 46 | 47 | # the key for the "demo" namespace 48 | key = os.environ["QUID_DEMO_KEY"] 49 | 50 | 51 | def verify_token(token): 52 | try: 53 | payload = jwt.decode(token, key, algorithms=['HS256']) 54 | except jwt.ExpiredSignatureError: 55 | return False 56 | print("Token payload", payload) 57 | date = datetime.fromtimestamp(payload["exp"]) 58 | print("Token expiration date", date) 59 | return True 60 | 61 | 62 | @app.route('/') 63 | @cross_origin() 64 | def main_route(): 65 | token = request.headers["Authorization"].split(" ")[1] 66 | is_valid = verify_token(token) 67 | if (is_valid is True): 68 | return {"response": "ok"} 69 | else: 70 | abort(401) 71 | ``` -------------------------------------------------------------------------------- /docsite/public/client/js.md: -------------------------------------------------------------------------------- 1 | ## Javascript client 2 | 3 | [![pub package](https://img.shields.io/npm/v/quidjs)](https://www.npmjs.com/package/quidjs) 4 | 5 | Install the [Quidjs library](https://github.com/teal-finance/quidjs). It transparently manage the requests to api 6 | servers. If a server returns a 401 Unauthorized response when an access token is expired the client library will 7 | request a new access token from a Quid server, using a refresh token, and will retry the request with the new 8 | access token 9 | 10 | ```bash 11 | yarn add quidjs 12 | # or 13 | npm install quidjs 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```typescript 19 | import { useQuidRequests } from "quidjs"; 20 | 21 | const quid = useQuidRequests({ 22 | namespace: "my_namespace", 23 | timeouts: { 24 | accessToken: "5m", 25 | refreshToken: "24h" 26 | }, 27 | quidUri: "https://localhost:8082", // quid server url 28 | serverUri: "https://localhost:8000", // url of your backend 29 | verbose: true, 30 | }); 31 | 32 | // login the user 33 | await quid.login("user", "pwd"); 34 | 35 | // use the quid instance to request json from the backend 36 | // GET request 37 | const response: Record = await quid.get>("/api/get"); 38 | console.log("Backend GET response:", response) 39 | // POST request 40 | const payload = {"foo": "bar"}; 41 | const response2: Record = await quid.post>("/api/post", payload); 42 | console.log("Backend POST response:", response2) 43 | ``` 44 | 45 | ## Examples 46 | 47 | - [Script src](https://github.com/teal-finance/quidjs/tree/master/examples/umd) 48 | - [Script module](https://github.com/teal-finance/quidjs/tree/master/examples/esm) -------------------------------------------------------------------------------- /docsite/public/server/python.md: -------------------------------------------------------------------------------- 1 | ## Python 2 | 3 | ### Decode tokens 4 | 5 | ```python 6 | import jwt 7 | 8 | try: 9 | payload = jwt.decode(token, key, algorithms=['HS256']) 10 | except jwt.ExpiredSignatureError: 11 | # ... 12 | ``` 13 | 14 | Payload example: 15 | 16 | ```json 17 | { 18 | "usr": "jane", 19 | "grp": ["group1", "group2"], 20 | "org": ["organization1", "organization2"], 21 | "exp": 1595950745 22 | } 23 | ``` 24 | 25 | Note: `"exp"` is the expiration timestamp in [Unix time](https://en.wikipedia.org/wiki/Unix_time) format (seconds since 1970). 26 | 27 | ### Flask server example 28 | 29 | ```python 30 | from datetime import datetime 31 | from flask import Flask, abort 32 | from flask_cors import CORS, cross_origin 33 | from flask import request 34 | import jwt 35 | import os 36 | 37 | """ 38 | To run: 39 | export FLASK_APP=server.py 40 | flask run 41 | """ 42 | 43 | app = Flask(__name__) 44 | cors = CORS(app) 45 | app.config['CORS_HEADERS'] = 'Content-Type' 46 | 47 | # the key for the "demo" namespace 48 | key = os.environ["QUID_DEMO_KEY"] 49 | 50 | 51 | def verify_token(token): 52 | try: 53 | payload = jwt.decode(token, key, algorithms=['HS256']) 54 | except jwt.ExpiredSignatureError: 55 | return False 56 | print("Token payload", payload) 57 | date = datetime.fromtimestamp(payload["exp"]) 58 | print("Token expiration date", date) 59 | return True 60 | 61 | 62 | @app.route('/') 63 | @cross_origin() 64 | def main_route(): 65 | token = request.headers["Authorization"].split(" ")[1] 66 | is_valid = verify_token(token) 67 | if (is_valid is True): 68 | return {"response": "ok"} 69 | else: 70 | abort(401) 71 | ``` -------------------------------------------------------------------------------- /ui/src/views/NamespaceView.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 47 | -------------------------------------------------------------------------------- /ui/src/components/admin/add/subviews/SearchForUsers.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 56 | -------------------------------------------------------------------------------- /ui/src/components/user/UserGroupsInfo.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /ui/src/components/user/AddUserIntoGroup.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /docs/server/nodejs.md: -------------------------------------------------------------------------------- 1 | ## Nodejs 2 | 3 | Decode an access token with a Nodejs server 4 | 5 | ```typescript 6 | import express, { Express, NextFunction, Request, Response } from 'express'; 7 | import bodyParser from 'body-parser'; 8 | import helmet from 'helmet'; 9 | import dotenv from 'dotenv'; 10 | import cors from 'cors'; 11 | import * as jwt from 'jsonwebtoken'; 12 | import { conf } from "./conf"; 13 | 14 | dotenv.config(); 15 | 16 | const PORT = process.env.PORT || 5714; 17 | const app: Express = express(); 18 | const keyBin = Buffer.from(conf.namespaceKey, 'hex'); 19 | 20 | app.use(cors({ origin: ["http://localhost:3000", "http://localhost:5173"], credentials: true })) 21 | app.use(helmet()); 22 | app.use(bodyParser.json()); 23 | app.use(bodyParser.urlencoded({ extended: true })); 24 | // jwt middleware 25 | app.use(verifyjwt); 26 | 27 | function verifyjwt(req: Request, res: Response, next: NextFunction) { 28 | const bearer = req.headers['authorization'] 29 | if (!bearer) { 30 | console.log("User has no token") 31 | return res.status(401).json('Unauthorized user') 32 | } 33 | const token = bearer.split(" ")[1] 34 | console.log("Verifying token", token) 35 | console.log("with hexadecimal key", conf.namespaceKey) 36 | try { 37 | jwt.verify(token, keyBin, function (err, decoded) { 38 | if (err) { 39 | console.log("Token unverified", err); 40 | return res.status(401).json('Wrong token') 41 | } else { 42 | console.log("Decoded token", decoded) 43 | } 44 | }); 45 | } catch (e) { 46 | return res.status(401).json('Token not valid') 47 | } 48 | next() 49 | } 50 | 51 | app.get('/', (req: Request, res: Response) => { 52 | //console.log("Request with auth header:", req.header("authorization")); 53 | res.send({ "response": "ok" }) 54 | }); 55 | 56 | app.listen(PORT, () => console.log(`Running on ${PORT} ⚡`)); 57 | ``` 58 | -------------------------------------------------------------------------------- /docsite/public/server/nodejs.md: -------------------------------------------------------------------------------- 1 | ## Nodejs 2 | 3 | Decode an access token with a Nodejs server 4 | 5 | ```typescript 6 | import express, { Express, NextFunction, Request, Response } from 'express'; 7 | import bodyParser from 'body-parser'; 8 | import helmet from 'helmet'; 9 | import dotenv from 'dotenv'; 10 | import cors from 'cors'; 11 | import * as jwt from 'jsonwebtoken'; 12 | import { conf } from "./conf"; 13 | 14 | dotenv.config(); 15 | 16 | const PORT = process.env.PORT || 5714; 17 | const app: Express = express(); 18 | const keyBin = Buffer.from(conf.namespaceKey, 'hex'); 19 | 20 | app.use(cors({ origin: ["http://localhost:3000", "http://localhost:5173"], credentials: true })) 21 | app.use(helmet()); 22 | app.use(bodyParser.json()); 23 | app.use(bodyParser.urlencoded({ extended: true })); 24 | // jwt middleware 25 | app.use(verifyjwt); 26 | 27 | function verifyjwt(req: Request, res: Response, next: NextFunction) { 28 | const bearer = req.headers['authorization'] 29 | if (!bearer) { 30 | console.log("User has no token") 31 | return res.status(401).json('Unauthorized user') 32 | } 33 | const token = bearer.split(" ")[1] 34 | console.log("Verifying token", token) 35 | console.log("with hexadecimal key", conf.namespaceKey) 36 | try { 37 | jwt.verify(token, keyBin, function (err, decoded) { 38 | if (err) { 39 | console.log("Token unverified", err); 40 | return res.status(401).json('Wrong token') 41 | } else { 42 | console.log("Decoded token", decoded) 43 | } 44 | }); 45 | } catch (e) { 46 | return res.status(401).json('Token not valid') 47 | } 48 | next() 49 | } 50 | 51 | app.get('/', (req: Request, res: Response) => { 52 | //console.log("Request with auth header:", req.header("authorization")); 53 | res.send({ "response": "ok" }) 54 | }); 55 | 56 | app.listen(PORT, () => console.log(`Running on ${PORT} ⚡`)); 57 | ``` 58 | -------------------------------------------------------------------------------- /ui/src/views/UserView.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 54 | -------------------------------------------------------------------------------- /docs/apidoc/overview.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | **Quid** is a [JWT][jwt] server (frontend + backend + client libraries) 4 | to manage Administrators, Users, **Refresh Tokens** and **Access Tokens** 5 | in independent **Namespaces** providing signature verification for the following algorithms: 6 | 7 | - HS256 = HMAC using SHA-256 8 | - HS384 = HMAC using SHA-384 9 | - HS512 = HMAC using SHA-512 10 | - RS256 = RSASSA-PKCS1-v1_5 using 2048-bits RSA key and SHA-256 11 | - RS384 = RSASSA-PKCS1-v1_5 using 2048-bits RSA key and SHA-384 12 | - RS512 = RSASSA-PKCS1-v1_5 using 2048-bits RSA key and SHA-512 13 | - ES256 = ECDSA using P-256 and SHA-256 14 | - ES384 = ECDSA using P-384 and SHA-384 15 | - ES512 = ECDSA using P-521 and SHA-512 16 | - EdDSA = Ed25519 17 | 18 | [jwt]: https://wikiless.org/wiki/JSON_Web_Token "JSON Web Token" 19 | 20 | ![Authentication flow chart](/img/authentication-flow.png) 21 | 22 | 1. First, the user logs in with **Namespace** + **Username** + **Password**. 23 | The **Namespace** is usually the final application name, 24 | represented by _Application API_ at the bottom of the previous diagram. 25 | 26 | 2. Then, the client (e.g. JS code) receives a **Refresh Token** 27 | that is usually valid for a few hours 28 | to avoid to log again during the working session. 29 | 30 | 3. The client sends this **Refresh Token** to get an **Access Token** 31 | that is valid for a short time, 32 | usually a few minutes, say 10 minutes. 33 | So the client must _refresh_ its **Access Token** every 10 minutes. 34 | 35 | 4. During these 10 minutes, 36 | the client can request the _Application API_ 37 | with the same **Access Token**. 38 | 39 | 5. When the _Application API_ receives a request from the client, 40 | it checks the [JWT][jwt] signature and expiration time. 41 | The **Access Token** is stateless: 42 | the _Application API_ does not need to store any information 43 | about the user (the **Access Token** content is enough). -------------------------------------------------------------------------------- /server/db/models.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "time" 4 | 5 | // base models to unpack data for Sqlx 6 | 7 | // Administrator : base model. 8 | type Administrator struct { 9 | ID int64 `json:"id" db:"id"` 10 | Name string `json:"name" db:"name"` 11 | UsrID int64 `json:"usr_id" db:"usr_id"` 12 | NsID int64 `json:"ns_id" db:"ns_id"` 13 | } 14 | 15 | // NonAdmin : base model. 16 | type NonAdmin struct { 17 | Name string `json:"name" db:"name"` 18 | UsrID int64 `json:"usr_id" db:"usr_id"` 19 | NsID int64 `json:"ns_id" db:"ns_id"` 20 | } 21 | 22 | type user struct { 23 | ID int64 `db:"id" json:"id"` 24 | Name string `db:"name" json:"name"` 25 | DateCreated time.Time `db:"date_created" json:"date_created"` 26 | PasswordHash string `db:"password" json:"-"` 27 | Namespace string `db:"namespace" json:"namespace"` 28 | Enabled bool `db:"enabled" json:"enabled"` 29 | } 30 | 31 | type userGroupName struct { 32 | Name string `db:"name" json:"name"` 33 | } 34 | 35 | type userOrgName struct { 36 | Name string `db:"name" json:"name"` 37 | } 38 | 39 | type namespace struct { 40 | Name string `db:"name" json:"name"` 41 | Alg string `db:"alg" json:"alg"` 42 | EncryptedAccessKey []byte `db:"access_key" json:"access_key"` 43 | EncryptedRefreshKey []byte `db:"refresh_key" json:"refresh_key"` 44 | MaxAccessTTL string `db:"max_access_ttl" json:"max_access_ttl"` 45 | MaxRefreshTTL string `db:"max_refresh_ttl" json:"max_refresh_ttl"` 46 | ID int64 `db:"id" json:"id"` 47 | PublicEndpointEnabled bool `db:"public_endpoint_enabled" json:"public_endpoint_enabled"` 48 | } 49 | 50 | type org struct { 51 | ID int64 `db:"id" json:"id"` 52 | Name string `db:"name" json:"name"` 53 | } 54 | -------------------------------------------------------------------------------- /docsite/public/apidoc/overview.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | **Quid** is a [JWT][jwt] server (frontend + backend + client libraries) 4 | to manage Administrators, Users, **Refresh Tokens** and **Access Tokens** 5 | in independent **Namespaces** providing signature verification for the following algorithms: 6 | 7 | - HS256 = HMAC using SHA-256 8 | - HS384 = HMAC using SHA-384 9 | - HS512 = HMAC using SHA-512 10 | - RS256 = RSASSA-PKCS1-v1_5 using 2048-bits RSA key and SHA-256 11 | - RS384 = RSASSA-PKCS1-v1_5 using 2048-bits RSA key and SHA-384 12 | - RS512 = RSASSA-PKCS1-v1_5 using 2048-bits RSA key and SHA-512 13 | - ES256 = ECDSA using P-256 and SHA-256 14 | - ES384 = ECDSA using P-384 and SHA-384 15 | - ES512 = ECDSA using P-521 and SHA-512 16 | - EdDSA = Ed25519 17 | 18 | [jwt]: https://wikiless.org/wiki/JSON_Web_Token "JSON Web Token" 19 | 20 | ![Authentication flow chart](/img/authentication-flow.png) 21 | 22 | 1. First, the user logs in with **Namespace** + **Username** + **Password**. 23 | The **Namespace** is usually the final application name, 24 | represented by _Application API_ at the bottom of the previous diagram. 25 | 26 | 2. Then, the client (e.g. JS code) receives a **Refresh Token** 27 | that is usually valid for a few hours 28 | to avoid to log again during the working session. 29 | 30 | 3. The client sends this **Refresh Token** to get an **Access Token** 31 | that is valid for a short time, 32 | usually a few minutes, say 10 minutes. 33 | So the client must _refresh_ its **Access Token** every 10 minutes. 34 | 35 | 4. During these 10 minutes, 36 | the client can request the _Application API_ 37 | with the same **Access Token**. 38 | 39 | 5. When the _Application API_ receives a request from the client, 40 | it checks the [JWT][jwt] signature and expiration time. 41 | The **Access Token** is stateless: 42 | the _Application API_ does not need to store any information 43 | about the user (the **Access Token** content is enough). -------------------------------------------------------------------------------- /ui/src/views/AdminsView.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /ui/src/components/TheSidebar.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 54 | -------------------------------------------------------------------------------- /ui/src/views/GroupView.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /server/api/quidadmin_middleware.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/teal-finance/quid/server" 7 | "github.com/teal-finance/quid/server/db" 8 | ) 9 | 10 | // quidAdminMiddleware : check the token claim to see if the user is admin. 11 | func quidAdminMiddleware(next http.Handler) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | tv, err := incorr.DecodeCookieToken(r) 14 | if err != nil { 15 | log.Warn("quidAdminMiddleware wants cookie", incorr.Cookie(0).Name, "but", err) 16 | gw.WriteErr(w, r, http.StatusUnauthorized, "missing or invalid incorruptible cookie", "want_cookie_name", incorr.Cookie(0).Name) 17 | return 18 | } 19 | 20 | values, err := tv.Get( 21 | tv.KString(keyUsername), 22 | tv.KInt64(KeyUsrID), 23 | tv.KString(keyNsName), 24 | tv.KInt64(keyNsID), 25 | tv.KString(keyAdminType)) 26 | if err != nil { 27 | log.Error(err) 28 | w.WriteHeader(http.StatusInternalServerError) 29 | return 30 | } 31 | 32 | username := values[keyUsername].String() 33 | usrID := values[KeyUsrID].Int64() 34 | namespace := values[keyNsName].String() 35 | nsID := values[keyNsID].Int64() 36 | adminType := values[keyAdminType].Bool() 37 | 38 | if server.AdminType(adminType) != server.QuidAdmin { 39 | log.ParamError("User '" + username + "' is not QuidAdmin") 40 | w.WriteHeader(http.StatusUnauthorized) 41 | return 42 | } 43 | 44 | userType, err := db.GetUserType(namespace, nsID, usrID) 45 | if err != nil { 46 | log.QueryError(err) 47 | gw.WriteErr(w, r, http.StatusUnauthorized, "DB error while getting user type", "ns_id", nsID, "uid", usrID) 48 | return 49 | } 50 | if userType != db.QuidAdmin { 51 | gw.WriteErr(w, r, http.StatusUnauthorized, 52 | log.Data("quidAdminMiddleware: u="+username+" is not Admin in database").Err().Error()) 53 | return 54 | } 55 | 56 | log.Param("quidAdminMiddleware OK u="+username+" (id=", usrID, ") ns="+namespace+" (id=", nsID, ")") 57 | r = tv.ToCtx(r) // save the token in the request context 58 | next.ServeHTTP(w, r) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /ui/src/components/admin/add/subviews/SelectUser.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quid", 3 | "description": "JWT server providing Refresh/Access tokens supporting HMAC, RSA, ECDSA and EdDSA", 4 | "repository": "https://github.com/teal-finance/quid", 5 | "logo": "https://raw.githubusercontent.com/teal-finance/quid/main/ui/public/img/logo.svg", 6 | "keywords": [ 7 | "JWT", 8 | "JSON Web Token", 9 | "Refresh token", 10 | "Access token", 11 | "HMAC", 12 | "RSA", 13 | "ECDSA", 14 | "EdDSA" 15 | ], 16 | "addons": [ 17 | { 18 | "plan": "heroku-postgresql" 19 | } 20 | ], 21 | "env": { 22 | "QUID_KEY": { 23 | "description": "An internal secret key for the Quid server used to encrypt sensitive data", 24 | "generator": "secret" 25 | }, 26 | "QUID_ADMIN_USR": { 27 | "description": "The Quid administrator username", 28 | "required": false, 29 | "value": "admin" 30 | }, 31 | "QUID_ADMIN_PWD": { 32 | "description": "The administrator password", 33 | "required": true 34 | }, 35 | "POSTGRES_USER": { 36 | "description": "The username used by the Quid server to connect to the Postgres server", 37 | "required": false, 38 | "value": "pguser" 39 | }, 40 | "POSTGRES_PASSWORD": { 41 | "description": "The database user password", 42 | "required": true 43 | }, 44 | "POSTGRES_DB": { 45 | "description": "The database name of the Quid server", 46 | "required": false, 47 | "value": "quid" 48 | }, 49 | "DB_HOST": { 50 | "description": "The network location of the Postgres server", 51 | "required": false, 52 | "value": "localhost" 53 | }, 54 | "DB_PORT": { 55 | "description": "The TCP port to send the requests to the Postgres server", 56 | "required": false, 57 | "value": "5432" 58 | }, 59 | "DB_URL": { 60 | "description": "The info to connect to the Postgres server", 61 | "required": false 62 | }, 63 | "WWW_DIR": { 64 | "description": "Folder of the web static files", 65 | "required": false, 66 | "value": "ui/dist" 67 | }, 68 | "PORT": { 69 | "description": "Listening port of the Quid server", 70 | "required": true, 71 | "value": "8090" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ui/public/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/public/img/logo-2em.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/api.ts: -------------------------------------------------------------------------------- 1 | import conf from "@/conf"; 2 | import { notify, user } from './state'; 3 | import { useApi, ApiResponse } from "@/packages/restmix/main"; 4 | import { UserStatusContract } from "./interface"; 5 | import Namespace from "./models/namespace"; 6 | 7 | const api = useApi({ serverUrl: conf.quidUrl }); 8 | api.onResponse(async (res: ApiResponse): Promise> => { 9 | console.log("On resp", JSON.stringify(res, null, " ")); 10 | if (!res.ok) { 11 | if ([401, 403].includes(res.status)) { 12 | console.warn("Unauthorized request", res.status, "from", res.url) 13 | } else if (res.status == 500) { 14 | console.warn("Server error", res.status, "from", res.url) 15 | } else { 16 | console.warn("Error", res.status, "from", res.url) 17 | } 18 | } 19 | return res 20 | }); 21 | 22 | async function checkStatus(): Promise<{ ok: boolean, status: UserStatusContract }> { 23 | let _data: UserStatusContract = {} as UserStatusContract; 24 | const res = await api.get("/status"); 25 | if (res.ok) { 26 | _data = res.data; 27 | } else { 28 | if (res.status == 401) { 29 | return { ok: false, status: {} as UserStatusContract } 30 | } 31 | throw new Error(res.data.toString()) 32 | } 33 | return { ok: true, status: _data } 34 | } 35 | 36 | async function adminLogin(namespace: string, username: string, password: string): Promise { 37 | const payload = { 38 | namespace: namespace, 39 | username: username, 40 | password: password, 41 | } 42 | const uri = "/admin_login"; 43 | console.log("Login", uri, JSON.stringify(payload)) 44 | 45 | const res = await api.post>(uri, payload); 46 | if (!res.ok) { 47 | console.log("RESP NOT OK", res); 48 | if (res.status === 401) { 49 | notify.warning("Login refused", "The server refused the login, please try again") 50 | return false 51 | } 52 | } 53 | // process response 54 | const ns = Namespace.empty(); 55 | ns.name = res.data.name; 56 | ns.id = res.data.id; 57 | if (namespace != 'quid') { 58 | user.changeNs(ns.toTableRow()); 59 | } else { 60 | user.type.value = "serverAdmin"; 61 | user.adminUrl = "/admin"; 62 | user.resetNs() 63 | } 64 | return true 65 | } 66 | 67 | export { api, adminLogin, checkStatus } 68 | -------------------------------------------------------------------------------- /ui/src/models/adminuser/adminuser.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/api"; 2 | import { notify } from "@/state"; 3 | import { AdminUserContract, AdminUserTable } from "./interface"; 4 | 5 | export default class AdminUser { 6 | id: number; 7 | name: string; 8 | usrId: number; 9 | 10 | constructor(row: AdminUserContract) { 11 | this.id = row.id; 12 | this.usrId = row.usr_id; 13 | this.name = row.name; 14 | } 15 | 16 | toTableRow(): AdminUserTable { 17 | const row = Object.create(this); 18 | row.actions = []; 19 | return row as AdminUserTable; 20 | } 21 | 22 | static async fetchAll(nsid: number): Promise> { 23 | const url = "/admin/nsadmin/nsall"; 24 | const data = new Array(); 25 | try { 26 | const payload = { ns_id: nsid } 27 | try { 28 | const resp = await api.post>(url, payload); 29 | resp.data.forEach((row) => data.push(new AdminUser(row).toTableRow())); 30 | } catch (e) { 31 | console.log("QERR", JSON.stringify(e, null, " ")) 32 | } 33 | } catch (e) { 34 | console.log("Err", e); 35 | 36 | throw e; 37 | } 38 | return data; 39 | } 40 | 41 | static async searchNonAdmins(nsid: number, username: string): Promise> { 42 | const url = "/admin/nsadmin/nonadmins"; 43 | const data = new Array(); 44 | try { 45 | const payload = { ns_id: nsid, pattern: username } 46 | const resp = await api.post<{ users: Array }>(url, payload); 47 | resp.data.users.forEach((row) => data.push(new AdminUser(row))); 48 | } catch (e) { 49 | console.log("Err", e); 50 | throw e; 51 | } 52 | return data; 53 | } 54 | 55 | static async fetchAdd(nsId: number, usrIds: Array) { 56 | try { 57 | await api.post("/admin/nsadmin/add", { 58 | ns_id: nsId, 59 | usr_ids: usrIds, 60 | }); 61 | } catch (e) { 62 | console.log(e) 63 | notify.error("Error adding admin users") 64 | } 65 | } 66 | 67 | static async delete(uid: number, nsid: number) { 68 | console.log("Delete", { 69 | ns_id: nsid, 70 | usr_id: uid, 71 | }) 72 | try { 73 | await api.post("/admin/nsadmin/delete", { 74 | ns_id: nsid, 75 | usr_id: uid, 76 | }); 77 | } catch (e) { 78 | console.log(e) 79 | notify.error("Error deleting admin users") 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quid-admin-ui", 3 | "version": "0.0.3", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "serve": "vite preview", 8 | "testinit": "npx playwright test --config=tests/playwright.init.config.js --project=firefox ./tests/init/login.spec.ts", 9 | "showtests": "showtests", 10 | "fftest": "playtest browser=firefox", 11 | "crtest": "playtest browser=chromium", 12 | "runtest": "runtest" 13 | }, 14 | "dependencies": { 15 | "emosd": "~0.4.0", 16 | "primeicons": "~7.0.0", 17 | "primevue": "~3.53.0", 18 | "restmix": "0.5.0", 19 | "vue": "~3.4.31", 20 | "vue-router": "4.4.0" 21 | }, 22 | "devDependencies": { 23 | "@iconify/json": "~2.2.227", 24 | "@iconify/vue": "~4.1.2", 25 | "@playwright/test": "^1.45.1", 26 | "@rollup/plugin-typescript": "^11.1.6", 27 | "@snowind/header": "~0.1.0", 28 | "@snowind/input": "~0.0.5", 29 | "@snowind/plugin": "~0.5.0", 30 | "@snowind/sidebar": "~0.1.0", 31 | "@snowind/state": "~0.2.0", 32 | "@snowind/stepper": "~0.1.0", 33 | "@snowind/subviews": "~0.1.0", 34 | "@snowind/switch": "~0.1.1", 35 | "@snowind/toast": "~0.0.1", 36 | "@tailwindcss/forms": "~0.5.7", 37 | "@types/js-cookie": "^3.0.6", 38 | "@types/node": "^20.14.10", 39 | "@vitejs/plugin-vue": "~5.0.5", 40 | "@vue/compiler-sfc": "~3.4.31", 41 | "autoprefixer": "~10.4.19", 42 | "babel-loader": "~9.1.3", 43 | "postcss": "~8.4.39", 44 | "sass": "~1.77.8", 45 | "tailwindcss": "~3.4.4", 46 | "tailwindcss-semantic-colors": "~0.2.0", 47 | "tslib": "~2.6.3", 48 | "typescript": "~5.5.3", 49 | "unplugin-icons": "~0.19.0", 50 | "unplugin-vue-components": "~0.27.2", 51 | "vite": "~5.3.3", 52 | "vue-loader": "~17.4.2", 53 | "vue-tsc": "~2.0.26" 54 | }, 55 | "engines": { 56 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 57 | }, 58 | "packageManager": "yarn@4.1.1+sha256.f3cc0eda8e5560e529c7147565b30faa43b4e472d90e8634d7134a37c7f59781", 59 | "license": "AGPL-3.0-or-later", 60 | "homepage": "https://Teal.Finance", 61 | "author": { 62 | "name": "Teal.Finance/Quid contributors", 63 | "email": "Teal.Finance@pm.me", 64 | "url": "https://github.com/teal-finance/quid/graphs/contributors" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "https://github.com/teal-finance/quid.git" 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/teal-finance/quid/issues" 72 | } 73 | } -------------------------------------------------------------------------------- /ui/src/components/namespace/EditTokenTtl.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 83 | 84 | -------------------------------------------------------------------------------- /ui/src/components/admin/add/AddAdmin.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 76 | -------------------------------------------------------------------------------- /crypt/crypt.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "encoding/hex" 8 | 9 | "github.com/teal-finance/emo" 10 | ) 11 | 12 | var log = emo.NewZone("crypt") 13 | 14 | // EncodingKey is used to encode each JWT secret key in the DB. 15 | var EncodingKey []byte 16 | 17 | const ( 18 | // nonceSize= 12 // AES-128 nonce is 12 bytes 19 | gcmTagSize = 16 // AES-GCM tag is 16 bytes 20 | ) 21 | 22 | // AesGcmEncryptHex : encrypt content. 23 | func AesGcmEncryptHex(plaintext string) (string, error) { 24 | b, err := AesGcmEncryptBin([]byte(plaintext)) 25 | return hex.EncodeToString(b), err 26 | } 27 | 28 | // AesGcmEncrypt : encrypt content. 29 | func AesGcmEncryptBin(plaintext []byte) ([]byte, error) { 30 | block, err := aes.NewCipher(EncodingKey) 31 | if err != nil { 32 | log.EncryptError("NewCipher AES", len(EncodingKey), "bytes", err) 33 | return nil, err 34 | } 35 | 36 | gcm, err := cipher.NewGCM(block) 37 | if err != nil { 38 | log.EncryptError("NewGCM", err) 39 | return nil, err 40 | } 41 | 42 | // all will contain the nonce (first 12 bytes) + the cypher text + the GCM tag 43 | all := make([]byte, gcm.NonceSize(), gcm.NonceSize()+len(plaintext)+gcmTagSize) 44 | 45 | // write a random nonce 46 | if _, err := rand.Read(all); err != nil { 47 | return nil, log.EncryptError("random iv generation", err).Err() 48 | } 49 | 50 | // write the cypher text after the nonce and appends the GCM tag 51 | all = gcm.Seal(all, all, plaintext, nil) 52 | return all, nil 53 | } 54 | 55 | // AesGcmDecryptHex : decrypt content. 56 | func AesGcmDecryptHex(encryptedString string) (string, error) { 57 | bytes, err := hex.DecodeString(encryptedString) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | plaintext, err := AesGcmDecryptBin(bytes) 63 | return string(plaintext), err 64 | } 65 | 66 | func AesGcmDecryptBin(encryptedBytes []byte) ([]byte, error) { 67 | block, err := aes.NewCipher(EncodingKey) 68 | if err != nil { 69 | log.DecryptError("NewCipher AES", len(EncodingKey), "bytes", err) 70 | return nil, err 71 | } 72 | 73 | gcm, err := cipher.NewGCM(block) 74 | if err != nil { 75 | log.DecryptError("NewGCM", err) 76 | return nil, err 77 | } 78 | 79 | iv := encryptedBytes[:gcm.NonceSize()] 80 | ciphertext := encryptedBytes[gcm.NonceSize():] 81 | dst := ciphertext[:0] 82 | 83 | // we are not subject to confused deputy attack => additionalData can be empty 84 | plaintext, err := gcm.Open(dst, iv, ciphertext, nil) 85 | if err != nil { 86 | log.DecryptError("Open ciphertext", len(ciphertext), "bytes", err) 87 | return nil, err 88 | } 89 | 90 | return plaintext, nil 91 | } 92 | -------------------------------------------------------------------------------- /docsite/src/components/TheHeader.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | -------------------------------------------------------------------------------- /server/api/nsadmin_middleware.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/teal-finance/incorruptible" 7 | "github.com/teal-finance/quid/server" 8 | "github.com/teal-finance/quid/server/db" 9 | ) 10 | 11 | // isNsAdmin checks that the requested namespace operation 12 | // matches the request ns admin permissions 13 | func isNsAdmin(r *http.Request, nsID int64) bool { 14 | tv, ok := incorruptible.FromCtx(r) 15 | if !ok { 16 | log.ParamError("VerifyAdminNs: missing Incorruptible token: cannot check Admin nsID=", nsID) 17 | return false 18 | } 19 | 20 | adminType := server.AdminType(tv.BoolIfAny(keyAdminType)) 21 | if adminType == server.QuidAdmin { 22 | log.Param("VerifyAdminNs OK: Incorruptible token contains IsNsAdmin=true => Do not check the nsID") 23 | return true 24 | } 25 | 26 | gotID, err := tv.Int64(keyNsID) 27 | if err != nil { 28 | log.ParamError("VerifyAdminNs: missing field nsID in Incorruptible token, want nsID=", nsID, err) 29 | return false 30 | } 31 | if gotID != nsID { 32 | log.ParamError("VerifyAdminNs: user is nsAdmin for", gotID, ", but not", nsID) 33 | return false 34 | } 35 | 36 | return true 37 | } 38 | 39 | // nsAdminMiddleware : check the token claim to see if the user is namespace admin. 40 | func nsAdminMiddleware(next http.Handler) http.Handler { 41 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 42 | tv, err := incorr.DecodeCookieToken(r) 43 | if err != nil { 44 | log.Warn("nsAdminMiddleware wants cookie", incorr.Cookie(0).Name, "but", err) 45 | gw.WriteErr(w, r, http.StatusUnauthorized, "missing or invalid incorruptible cookie", "want_cookie_name", incorr.Cookie(0).Name) 46 | return 47 | } 48 | 49 | values, err := tv.Get( 50 | tv.KString(keyUsername), 51 | tv.KInt64(KeyUsrID), 52 | tv.KString(keyNsName), 53 | tv.KInt64(keyNsID)) 54 | if err != nil { 55 | log.Error(err) 56 | w.WriteHeader(http.StatusInternalServerError) 57 | return 58 | } 59 | 60 | username := values[keyUsername].String() 61 | usrID := values[KeyUsrID].Int64() 62 | namespace := values[keyNsName].String() 63 | nsID := values[keyNsID].Int64() 64 | 65 | userType, err := db.GetUserType(namespace, nsID, usrID) 66 | if err != nil { 67 | log.QueryError("nsAdminMiddleware:", err) 68 | w.WriteHeader(http.StatusInternalServerError) 69 | return 70 | } 71 | if userType == db.UserNoAdmin { 72 | log.ParamError("nsAdminMiddleware: u=" + username + " is admin, but not for ns=" + namespace) 73 | w.WriteHeader(http.StatusUnauthorized) 74 | return 75 | } 76 | 77 | log.RequestPost("nsAdminMiddleware OK u="+username+" (id=", usrID, ") ns="+namespace+" (id=", nsID, ")") 78 | r = tv.ToCtx(r) // save the token in the request context 79 | next.ServeHTTP(w, r) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /server/responses.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "time" 4 | 5 | // Namespace : base model. 6 | type Namespace struct { 7 | ID int64 `json:"id" db:"id"` 8 | Name string `json:"name" db:"name"` 9 | Alg string `json:"alg" db:"alg"` 10 | RefreshKey []byte `json:"-" db:"refresh_key"` 11 | AccessKey []byte `json:"-" db:"access_key"` 12 | MaxRefreshTTL string `json:"max_refresh_ttl" db:"max_refresh_ttl"` 13 | MaxAccessTTL string `json:"max_access_ttl" db:"max_access_ttl"` 14 | Enabled bool `json:"public_endpoint_enabled" db:"public_endpoint_enabled"` 15 | } 16 | 17 | // NamespaceInfo : base model. 18 | type NamespaceInfo struct { 19 | Groups []Group `json:"groups"` 20 | NumUsers int `json:"num_users"` 21 | } 22 | 23 | // User : base model. 24 | type User struct { 25 | DateCreated time.Time `json:"date_created" db:"date_created"` 26 | ID int64 `json:"id" db:"id"` 27 | Name string `json:"name" db:"name"` 28 | PasswordHash string `json:"-" db:"password_hash"` 29 | Namespace string `json:"namespace,omitempty" db:"namespace"` 30 | Org string `json:"org,omitempty" db:"org"` 31 | Groups []Group `json:"groups,omitempty" db:"groups"` 32 | Enabled bool `json:"enabled" db:"enabled"` 33 | } 34 | 35 | // groupNames : list the names of all groups. 36 | // Deprecated because this function is not used. 37 | func (user User) groupNames() []string { 38 | names := make([]string, 0, len(user.Groups)) 39 | for _, g := range user.Groups { 40 | names = append(names, g.Name) 41 | } 42 | return names 43 | } 44 | 45 | // Group : base model. 46 | type Group struct { 47 | Name string `json:"name"` 48 | Namespace string `json:"namespace"` 49 | ID int64 `json:"id"` 50 | } 51 | 52 | // // UserGroup : base model. 53 | // type UserGroup struct { 54 | // ID int32 `json:"id"` 55 | // UsrID int64 `json:"usr_id"` 56 | // GrpID int64 `json:"grp_id"` 57 | // } 58 | 59 | // Org : base model. 60 | type Org struct { 61 | Name string `json:"name"` 62 | ID int64 `json:"id"` 63 | } 64 | 65 | type StatusResponse struct { 66 | AdminType string `json:"admin_type"` 67 | Username string `json:"username"` 68 | Ns NSInfo `json:"ns"` 69 | } 70 | 71 | type NSInfo struct { 72 | ID int64 `json:"id"` 73 | Name string `json:"name"` 74 | } 75 | 76 | type AdminType bool 77 | 78 | const ( 79 | QuidAdmin AdminType = false 80 | NsAdmin AdminType = true 81 | ) 82 | 83 | func (t AdminType) String() string { 84 | if t == QuidAdmin { 85 | return "QuidAdmin" 86 | } 87 | return "NsAdmin" 88 | } 89 | -------------------------------------------------------------------------------- /ui/public/img/logo-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Quid 12 | Authentication Server 13 | Refresh Access JWT 14 | 15 | -------------------------------------------------------------------------------- /server/requests.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type PasswordRequest struct { 4 | Username string `json:"username"` 5 | Password string `json:"password"` 6 | Namespace string `json:"namespace"` 7 | } 8 | 9 | type UserCreation struct { 10 | Username string `json:"username"` 11 | Password string `json:"password"` 12 | NsID int64 `json:"ns_id"` 13 | } 14 | 15 | type UserSetEnabled struct { 16 | ID int64 `json:"id"` 17 | Enabled bool `json:"enabled"` 18 | } 19 | 20 | type GroupCreation struct { 21 | Name string `json:"name"` 22 | NsID int64 `json:"ns_id"` 23 | } 24 | 25 | type NamespaceIDRequest struct { 26 | NsID int64 `json:"ns_id"` 27 | } 28 | 29 | type RefreshMaxTTLRequest struct { 30 | ID int64 `json:"id"` 31 | RefreshMaxTTL string `json:"refresh_max_ttl"` 32 | } 33 | 34 | type MaxTTLRequest struct { 35 | ID int64 `json:"id"` 36 | MaxTTL string `json:"max_ttl"` 37 | } 38 | 39 | type InfoRequest struct { 40 | ID int64 `json:"id"` 41 | EncodingForm string `json:"encoding_form"` 42 | } 43 | 44 | type NameRequest struct { 45 | Name string `json:"name"` 46 | } 47 | 48 | type Availability struct { 49 | ID int64 `json:"id"` 50 | Enable bool 51 | } 52 | 53 | type NamespaceCreation struct { 54 | Name string `json:"name"` 55 | Alg string `json:"alg"` 56 | MaxTTL string `json:"max_ttl"` 57 | RefreshMaxTTL string `json:"refresh_max_ttl"` 58 | EnableEndpoint bool `json:"enable_endpoint"` 59 | } 60 | 61 | type NonAdminUsersRequest struct { 62 | Pattern string `json:"pattern"` 63 | NsID int64 `json:"ns_id"` 64 | } 65 | 66 | type AdministratorsCreation struct { 67 | UsrIDs []int64 `json:"usr_ids"` 68 | NsID int64 `json:"ns_id"` 69 | } 70 | 71 | type AdministratorDeletion struct { 72 | UsrID int64 `json:"usr_id"` 73 | NsID int64 `json:"ns_id"` 74 | } 75 | 76 | type AccessTokenRequest struct { 77 | RefreshToken string `json:"refresh_token"` 78 | Namespace string `json:"namespace"` 79 | } 80 | 81 | type AccessTokenValidationRequest struct { 82 | AccessToken string `json:"access_token"` 83 | Namespace string `json:"namespace"` 84 | } 85 | 86 | type NamespaceRequest struct { 87 | Namespace string `json:"namespace"` 88 | EncodingForm string `json:"encoding_form"` 89 | } 90 | 91 | type UserOrgRequest struct { 92 | UsrID int64 `json:"usr_id"` 93 | OrgID int64 `json:"org_id"` 94 | } 95 | 96 | type UserGroupRequest struct { 97 | UsrID int64 `json:"usr_id"` 98 | GrpID int64 `json:"grp_id"` 99 | NsID int64 `json:"ns_id"` 100 | } 101 | 102 | type UserRequest struct { 103 | ID int64 `json:"id"` 104 | NsID int64 `json:"ns_id"` 105 | } 106 | 107 | type PublicKeyResponse struct { 108 | Alg string `json:"alg"` 109 | Key []byte `json:"key"` 110 | } 111 | -------------------------------------------------------------------------------- /ui/src/components/TheTopbar.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/teal-finance/quid 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 7 | github.com/cristalhq/base64 v0.1.2 8 | github.com/go-chi/chi/v5 v5.0.12 9 | github.com/golang-jwt/jwt/v4 v4.5.0 10 | github.com/jmoiron/sqlx v1.4.0 11 | github.com/lib/pq v1.10.9 12 | github.com/manifoldco/promptui v0.9.0 13 | github.com/spf13/viper v1.19.0 14 | github.com/teal-finance/emo v0.0.0-20240610104517-58d37361ce25 15 | github.com/teal-finance/garcon v0.35.1 16 | github.com/teal-finance/incorruptible v0.0.0-20240610111019-e80aff374f9b 17 | golang.org/x/crypto v0.24.0 18 | ) 19 | 20 | require ( 21 | github.com/JohannesKaufmann/html-to-markdown v1.6.0 // indirect 22 | github.com/PuerkitoBio/goquery v1.9.2 // indirect 23 | github.com/andybalholm/brotli v1.1.0 // indirect 24 | github.com/andybalholm/cascadia v1.3.2 // indirect 25 | github.com/beorn7/perks v1.0.1 // indirect 26 | github.com/carlmjohnson/flagx v0.22.2 // indirect 27 | github.com/carlmjohnson/versioninfo v0.22.5 // indirect 28 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 29 | github.com/chzyer/readline v1.5.1 // indirect 30 | github.com/felixge/fgprof v0.9.4 // indirect 31 | github.com/fsnotify/fsnotify v1.7.0 // indirect 32 | github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect 33 | github.com/hashicorp/hcl v1.0.0 // indirect 34 | github.com/josharian/intern v1.0.0 // indirect 35 | github.com/klauspost/compress v1.17.8 // indirect 36 | github.com/magiconair/properties v1.8.7 // indirect 37 | github.com/mailru/easyjson v0.7.7 // indirect 38 | github.com/minio/highwayhash v1.0.2 // indirect 39 | github.com/mitchellh/mapstructure v1.5.0 // indirect 40 | github.com/mtraver/base91 v1.0.0 // indirect 41 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 42 | github.com/pkg/profile v1.7.0 // indirect 43 | github.com/prometheus/client_golang v1.19.1 // indirect 44 | github.com/prometheus/client_model v0.6.1 // indirect 45 | github.com/prometheus/common v0.54.0 // indirect 46 | github.com/prometheus/procfs v0.15.1 // indirect 47 | github.com/rs/cors v1.11.0 // indirect 48 | github.com/sagikazarmark/locafero v0.6.0 // indirect 49 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 50 | github.com/sourcegraph/conc v0.3.0 // indirect 51 | github.com/spf13/afero v1.11.0 // indirect 52 | github.com/spf13/cast v1.6.0 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/subosito/gotenv v1.6.0 // indirect 55 | go.uber.org/multierr v1.11.0 // indirect 56 | golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect 57 | golang.org/x/net v0.26.0 // indirect 58 | golang.org/x/sys v0.21.0 // indirect 59 | golang.org/x/text v0.16.0 // indirect 60 | golang.org/x/time v0.5.0 // indirect 61 | google.golang.org/protobuf v1.34.1 // indirect 62 | gopkg.in/ini.v1 v1.67.0 // indirect 63 | gopkg.in/yaml.v3 v3.0.1 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /ui/src/components/user/AddUser.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 89 | -------------------------------------------------------------------------------- /ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 59 | 60 | 96 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '22 22 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on= 27 | runs-on: ubuntu-latest 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: [ 'go', 'javascript' ] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 38 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | 53 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 54 | # queries: security-extended,security-and-quality 55 | 56 | 57 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 58 | # If this step fails, then you should remove it and run the build manually (see below) 59 | - name: Autobuild 60 | uses: github/codeql-action/autobuild@v2 61 | 62 | # ℹ️ Command-line programs to run using the OS shell. 63 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 64 | 65 | # If the Autobuild fails above, remove it and uncomment the following three lines. 66 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 67 | 68 | # - run: | 69 | # echo "Run, Build Application using script" 70 | # ./location_of_script_within_repo/buildscript.sh 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v2 74 | -------------------------------------------------------------------------------- /server/db/init.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/manifoldco/promptui" 8 | 9 | "github.com/teal-finance/quid/server" 10 | "github.com/teal-finance/quid/tokens" 11 | ) 12 | 13 | var ( 14 | ErrUsernameTooShort = errors.New("username must have more than 2 characters") 15 | ErrPasswordTooShort = errors.New("password must have more than 5 characters") 16 | ) 17 | 18 | func CreateQuidAdminIfMissing(username, password string) error { 19 | exist, err := NamespaceExists("quid") 20 | if err != nil { 21 | return err 22 | } 23 | 24 | var nsID int64 25 | if exist { 26 | nsID, err = SelectNsID("quid") 27 | } else { 28 | log.V().Data(`Creating the "quid" namespace`) 29 | algo := "HS256" 30 | accessKey := tokens.GenerateKeyHMAC(256) 31 | refreshKey := tokens.GenerateKeyHMAC(256) 32 | nsID, err = CreateNamespace("quid", "6m", "24h", algo, accessKey, refreshKey, false) 33 | } 34 | if err != nil { 35 | return err 36 | } 37 | 38 | exist, err = GroupExists("quid_admin", nsID) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | var gid int64 44 | if exist { 45 | var g server.Group 46 | g, err = SelectGroup("quid_admin", nsID) 47 | gid = g.ID 48 | } else { 49 | log.V().Data(`Creating the "quid_admin" group`) 50 | gid, err = CreateGroup("quid_admin", nsID) 51 | } 52 | if err != nil { 53 | return err 54 | } 55 | 56 | n, err := CountUsersInGroup(gid) 57 | if err != nil { 58 | return err 59 | } 60 | if n > 0 { 61 | log.Dataf(`Quid Admin already created (%d user in "quid_admin" group)`, n) 62 | return nil 63 | } 64 | 65 | if username == "" { 66 | username, err = promptForUsername() 67 | if err != nil { 68 | return err 69 | } 70 | } 71 | 72 | if password == "" { 73 | password, err = promptForPassword() 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | 79 | log.V().Dataf("Create the Quid Admin usr=%q pwd=%d bytes", username, len(password)) 80 | u, err := CreateUser(username, password, nsID) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | return AddUserInGroup(u.ID, gid) 86 | } 87 | 88 | func promptForUsername() (string, error) { 89 | fmt.Println(` 90 | Enter the Quid Admin username. 91 | ` + ErrUsernameTooShort.Error()) 92 | 93 | prompt := promptui.Prompt{ 94 | Label: "Username", 95 | Default: "admin", 96 | Validate: func(s string) error { 97 | if len(s) <= 2 { 98 | return ErrUsernameTooShort 99 | } 100 | return nil 101 | }, 102 | } 103 | 104 | username, err := prompt.Run() 105 | if err != nil { 106 | log.ParamError(err) 107 | } 108 | return username, err 109 | } 110 | 111 | func promptForPassword() (string, error) { 112 | fmt.Println(` 113 | Enter the Quid Admin password. 114 | ` + ErrPasswordTooShort.Error()) 115 | 116 | prompt := promptui.Prompt{ 117 | Label: "Password", 118 | Default: "", 119 | Validate: func(input string) error { 120 | if len(input) <= 5 { 121 | return ErrPasswordTooShort 122 | } 123 | return nil 124 | }, 125 | Mask: '*', 126 | } 127 | 128 | password, err := prompt.Run() 129 | if err != nil { 130 | log.ParamError(err) 131 | } 132 | return password, err 133 | } 134 | -------------------------------------------------------------------------------- /server/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "unicode" 9 | 10 | "github.com/jmoiron/sqlx" 11 | 12 | // pg import. 13 | _ "github.com/lib/pq" 14 | 15 | "github.com/teal-finance/emo" 16 | ) 17 | 18 | var db *sqlx.DB 19 | 20 | var log = emo.NewZone("db") 21 | 22 | // Connect : connect to the db. 23 | func Connect(url string) error { 24 | url = strings.Replace(url, "postgresql://", "postgres://", 1) 25 | _db, err := sqlx.Connect("postgres", url) 26 | if err != nil { 27 | log.Warn(err) 28 | return err 29 | } 30 | 31 | db = _db 32 | return nil 33 | } 34 | 35 | // DropTablesIndexes deletes all tables and indexes from DB. 36 | func DropTablesIndexes() error { 37 | result, err := db.Exec(dropAll) 38 | if err != nil { 39 | log.Warn(err) 40 | return err 41 | } 42 | 43 | if result == nil { 44 | return errors.New("DropTablesIndexes: result is nil") 45 | } 46 | 47 | n, err := result.RowsAffected() 48 | if err != nil { 49 | log.Warn("DropTablesIndexes RowsAffected:", err) 50 | return err 51 | } 52 | 53 | log.Dataf("Dropped tables and indexes (if exist). RowsAffected=%d", n) 54 | return nil 55 | } 56 | 57 | // DropDatabase executes "DROP DATABASE $POSTGRES_DB;". 58 | func DropDatabase(dbName string) error { 59 | if p := isAlphaNum(dbName); p >= 0 { 60 | return fmt.Errorf("the database name must be composed of letters and digits only, "+ 61 | "but got %q containing an invalid character at position %d", dbName, p) 62 | } 63 | 64 | result, err := db.Exec("DROP DATABASE " + dbName + ";") 65 | if err != nil { 66 | log.Warn(err) 67 | return err 68 | } 69 | 70 | if result == nil { 71 | return errors.New("DropDatabase: result is nil") 72 | } 73 | 74 | n, err := result.RowsAffected() 75 | if err != nil { 76 | log.Warn("DropDatabase RowsAffected:", err) 77 | return err 78 | } 79 | 80 | log.Dataf("Dropped database. RowsAffected=%d", n) 81 | return nil 82 | } 83 | 84 | // CreateTablesIndexesIfMissing : execute the schema. 85 | func CreateTablesIndexesIfMissing() error { 86 | result, err := db.Exec(schema) 87 | if err != nil { 88 | log.Warn(err) 89 | return err 90 | } 91 | 92 | if result == nil { 93 | return errors.New("CreateTablesIndexes: result is nil") 94 | } 95 | 96 | n, err := result.RowsAffected() 97 | if err != nil { 98 | log.Warn("CreateTablesIndexes RowsAffected:", err) 99 | return err 100 | } 101 | 102 | log.Dataf("Created tables and indexes (if not exist). RowsAffected=%d", n) 103 | return nil 104 | } 105 | 106 | func isAlphaNum(s string) int { 107 | for i, r := range s { 108 | if unicode.IsLetter(r) { 109 | continue 110 | } 111 | if unicode.IsDigit(r) { 112 | continue 113 | } 114 | return i 115 | } 116 | return -1 117 | } 118 | 119 | func getFirstID(name string, rows *sql.Rows) (int64, error) { 120 | if !rows.Next() { 121 | return 0, log.S(1).QueryErrorf("no name=%q", name).Err() 122 | } 123 | 124 | var idAny any 125 | err := rows.Scan(&idAny) 126 | if err != nil { 127 | log.S(1).QueryError("name=", name, ":", err) 128 | return 0, err 129 | } 130 | 131 | id, ok := idAny.(int64) 132 | if !ok { 133 | return 0, log.S(1).QueryError("name=", name, ": cannot convert", idAny, " into int64").Err() 134 | } 135 | 136 | return id, nil 137 | } 138 | -------------------------------------------------------------------------------- /ui/src/models/group/group.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/api"; 2 | import { GroupContract } from "./contract"; 3 | import { GroupTable } from "./interface"; 4 | import { user } from "@/state"; 5 | 6 | export default class Group { 7 | id: number; 8 | name: string; 9 | 10 | constructor({ id, name }: { id: number, name: string }) { 11 | this.id = id; 12 | this.name = name; 13 | } 14 | 15 | // ************************* 16 | // factory constructors 17 | // ************************* 18 | 19 | static fromContract(data: GroupContract): Group { 20 | return new Group({ id: data.id, name: data.name }) 21 | } 22 | 23 | // ************************* 24 | // methods 25 | // ************************* 26 | 27 | toTableRow(): GroupTable { 28 | const row: GroupTable = { 29 | id: this.id, 30 | name: this.name, 31 | actions: [], 32 | }; 33 | return row; 34 | } 35 | 36 | // ************************* 37 | // static methods 38 | // ************************* 39 | 40 | static async fetchAll(nsid: number): Promise> { 41 | const url = user.adminUrl + "/groups/nsall"; 42 | const ns = new Array(); 43 | try { 44 | const payload = { ns_id: nsid } 45 | const resp = await api.post>(url, payload, false, true); 46 | resp.data.forEach((row) => { 47 | //console.log(row) 48 | ns.push(new Group(row).toTableRow()) 49 | }); 50 | } catch (e) { 51 | console.log("Err", e); 52 | throw e; 53 | } 54 | return ns; 55 | } 56 | 57 | static async fetchUserGroups(uid: number) { 58 | const url = user.adminUrl + "/users/groups"; 59 | const data = new Array(); 60 | try { 61 | const payload = { id: uid, ns_id: user.namespace.value.id } 62 | const resp = await api.post<{ groups: Array | null }>(url, payload); 63 | //console.log("RESP", JSON.stringify(resp.groups, null, " ")) 64 | if (resp.data.groups) { 65 | if (resp.data.groups.length > 0) { 66 | resp.data.groups.forEach((row) => { 67 | data.push(new Group(row).toTableRow()) 68 | }); 69 | } 70 | } 71 | } catch (e) { 72 | console.log("Err", e); 73 | throw e; 74 | } 75 | return data; 76 | } 77 | 78 | static async addUserToGroup(uid: number, gid: number) { 79 | const url = user.adminUrl + "/groups/add_user"; 80 | try { 81 | const payload = { 82 | usr_id: uid, 83 | grp_id: gid, 84 | ns_id: user.namespace.value.id 85 | } 86 | await api.post(url, payload); 87 | } catch (e) { 88 | console.log("Err", e); 89 | throw e; 90 | } 91 | } 92 | 93 | static async removeUserFromGroup(uid: number, gid: number) { 94 | const url = user.adminUrl + "/groups/remove_user"; 95 | try { 96 | const payload = { 97 | usr_id: uid, 98 | grp_id: gid, 99 | ns_id: user.namespace.value.id 100 | } 101 | await api.post(url, payload); 102 | } catch (e) { 103 | console.log("Err", e); 104 | throw e; 105 | } 106 | } 107 | 108 | static async delete(id: number) { 109 | await api.post(user.adminUrl + "/groups/delete", { 110 | id: id, 111 | ns_id: user.namespace.value.id 112 | }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ui/src/components/user/UserDatatable.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /server/db/nsadmin.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | _ "github.com/lib/pq" 5 | ) 6 | 7 | // SelectAdministrators : get the admin users in a namespace. 8 | func SelectAdministrators(nsID int64) ([]Administrator, error) { 9 | q := "SELECT administrators.id, administrators.usr_id, administrators.ns_id, users.name " + 10 | "FROM administrators " + 11 | "LEFT OUTER JOIN users on users.id=administrators.usr_id " + 12 | "LEFT OUTER JOIN namespaces on namespaces.id=administrators.ns_id " + 13 | "WHERE namespaces.id=$1" 14 | log.Query(q, "nsID=", nsID) 15 | 16 | var data []Administrator 17 | err := db.Select(&data, q, nsID) 18 | if err != nil { 19 | log.Error(err) 20 | return nil, err 21 | } 22 | 23 | return data, nil 24 | } 25 | 26 | // SelectNonAdministrators : find non admin users in a namespace 27 | func SelectNonAdministrators(nsID int64, qs string) ([]NonAdmin, error) { 28 | q := "SELECT users.id as usr_id, users.name as name, namespaces.id as ns_id FROM users " + 29 | "JOIN namespaces ON users.ns_id = namespaces.id " + 30 | "WHERE (namespaces.id = $1 AND users.name LIKE E'" + qs + "%') " + 31 | "AND users.id NOT IN ( " + 32 | "SELECT administrators.usr_id as id " + 33 | "FROM administrators " + 34 | "LEFT OUTER JOIN users on users.id = administrators.usr_id " + 35 | "LEFT OUTER JOIN namespaces on namespaces.id = administrators.ns_id" + 36 | " )" 37 | log.Query(q, "nsID=", nsID) 38 | 39 | var data []NonAdmin 40 | err := db.Select(&data, q, nsID) 41 | if err != nil { 42 | log.Error(err) 43 | return nil, err 44 | } 45 | 46 | log.Debug("Data", data) 47 | return data, nil 48 | } 49 | 50 | // CreateAdministrator : create an admin user. 51 | func CreateAdministrator(nsID, usrID int64) error { 52 | q := "INSERT INTO administrators(ns_id, usr_id) VALUES($1,$2)" 53 | 54 | _, err := db.Query(q, nsID, usrID) 55 | if err != nil { 56 | log.QueryError(err) 57 | } 58 | return err 59 | } 60 | 61 | // IsUserAnAdmin : check if an admin user exists. 62 | func IsUserAnAdmin(usrID, nsID int64) (bool, error) { 63 | q := "SELECT COUNT(id) FROM administrators WHERE (ns_id=$1 AND usr_id=$2)" 64 | 65 | var n int 66 | err := db.Get(&n, q, nsID, usrID) 67 | if err != nil { 68 | log.Warn(err) 69 | return false, err 70 | } 71 | 72 | exists := (n > 0) 73 | return exists, nil 74 | } 75 | 76 | // DeleteAdministrator : delete an admin user for a namespace. 77 | func DeleteAdministrator(usrID, nsID int64) error { 78 | q := "DELETE FROM administrators WHERE (usr_id=$1 AND ns_id=$2)" 79 | 80 | log.Data(q, usrID, nsID) 81 | 82 | tx := db.MustBegin() 83 | tx.MustExec(q, usrID, nsID) 84 | 85 | err := tx.Commit() 86 | if err != nil { 87 | log.Warn(err) 88 | } 89 | return err 90 | } 91 | 92 | type UserType int 93 | 94 | const ( 95 | UserNoAdmin = iota 96 | NsAdmin 97 | QuidAdmin 98 | ) 99 | 100 | // GetUserType checks if a user is 101 | func GetUserType(nsName string, nsID, usrID int64) (UserType, error) { 102 | if nsName == "quid" { 103 | // check if the user is in the quid admin group 104 | exists, err := IsUserInAdminGroup(usrID, nsID) 105 | if (err != nil) || !exists { 106 | return UserNoAdmin, err 107 | } 108 | return QuidAdmin, nil 109 | } 110 | 111 | // check if the user is namespace administrator 112 | exists, err := IsUserAnAdmin(usrID, nsID) 113 | if (err != nil) || !exists { 114 | return UserNoAdmin, err 115 | } 116 | return NsAdmin, nil 117 | } 118 | -------------------------------------------------------------------------------- /doc/setup_db.md: -------------------------------------------------------------------------------- 1 | # Setup the PostgreSQL database 2 | 3 | ## Check PostgreSQL 4 | 5 | Install, then check the PostgreSQL status and port: 6 | 7 | ```sh 8 | $ sudo apt install postgresql 9 | $ sudo service postgresql status 10 | $ ss -nlt | grep 5432 11 | LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* 12 | LISTEN 0 244 [::1]:5432 [::]:* 13 | ``` 14 | 15 | Note: By default, Quid connects to the PostgreSQL server on port 5432. 16 | 17 | ## Configuration 18 | 19 | Quid can be configured with command line options, environnement variables and configuration file. 20 | 21 | The following table concerns only the database: 22 | 23 | | Command line options | Environnement variables | `config.json` | Default value | 24 | | -------------------- | ----------------------- | ------------------------- | --------------------------------------------------------------- | 25 | | `-db-user` | `POSTGRES_USER` | `"db_user": "pguser",` | `pguser` | 26 | | `-db-pass` | `POSTGRES_PASSWORD` | `"db_password": "xxxxx",` | `myDBpwd` | 27 | | `-db-name` | `POSTGRES_DB` | `"db_name": "quid",` | `quid` | 28 | | `-db-host` | `DB_HOST` | | `localhost` | 29 | | `-db-port` | `DB_PORT` | | `5432` | 30 | | `-db-url` | `DB_URL` | | `postgres://pguser:myDBpwd@localhost:5432/quid?sslmode=disable` | 31 | 32 | When the `-db-url` option or the `DB_URL` env. var. is used, 33 | Quid does not uses the other options, env. vars and the configuration file. 34 | The inverse, when `-db-url` and `DB_URL` are still set to the default value, 35 | Quid rewrites the `-db-url` `DB_URL` using the following formula: 36 | 37 | ```py 38 | URL = "postgres://{USR}:{PWD}@{HOST}:{PORT}/{NAME}?sslmode=disable" 39 | ``` 40 | 41 | ## Setup in one command line 42 | 43 | ```sql 44 | sudo -u postgres psql -c "CREATE USER pguser WITH PASSWORD 'myDBpwd'" -c "CREATE DATABASE quid" -c "GRANT ALL PRIVILEGES ON DATABASE quid TO pguser" 45 | ``` 46 | 47 | output: 48 | 49 | ```sql 50 | CREATE ROLE 51 | CREATE DATABASE 52 | GRANT 53 | ``` 54 | 55 | The previous command line perform all the setup operations described in the following chapters. 56 | 57 | ## Create user and database 58 | 59 | If you do not have already created a privileged user, create it: 60 | 61 | ```sql 62 | $ sudo -u postgres psql 63 | postgres=# CREATE USER pguser WITH PASSWORD 'myDBpwd'; 64 | CREATE ROLE 65 | postgres=# exit 66 | ``` 67 | 68 | Update the `config.json` file: 69 | 70 | ```json 71 | "db_user": "pguser", 72 | "db_password": "myDBpwd", 73 | ``` 74 | 75 | ## Create the `quid` database 76 | 77 | ```sql 78 | $ sudo -u postgres psql 79 | postgres=# CREATE DATABASE quid; 80 | CREATE DATABASE 81 | postgres=# exit 82 | ``` 83 | 84 | ## Set the database permissions 85 | 86 | ```sql 87 | $ sudo -u postgres psql 88 | postgres=# GRANT ALL PRIVILEGES ON DATABASE quid TO pguser; 89 | GRANT 90 | postgres=# exit 91 | ``` 92 | 93 | The previous statement may be replaced by: 94 | 95 | ```sql 96 | $ sudo -u postgres psql 97 | postgres=# \c quid 98 | You are now connected to database "quid" as user "postgres". 99 | quid-# GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO pguser; 100 | GRANT 101 | quid=# exit 102 | ``` 103 | -------------------------------------------------------------------------------- /ui/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | ActionButton: typeof import('./src/components/widgets/ActionButton.vue')['default'] 11 | AddAdmin: typeof import('./src/components/admin/add/AddAdmin.vue')['default'] 12 | AddGroup: typeof import('./src/components/group/AddGroup.vue')['default'] 13 | AddNamespace: typeof import('./src/components/namespace/AddNamespace.vue')['default'] 14 | AddOrg: typeof import('./src/components/org/AddOrg.vue')['default'] 15 | AddUser: typeof import('./src/components/user/AddUser.vue')['default'] 16 | AddUserIntoGroup: typeof import('./src/components/user/AddUserIntoGroup.vue')['default'] 17 | AdminDatatable: typeof import('./src/components/admin/AdminDatatable.vue')['default'] 18 | CardsGrid: typeof import('./src/components/widgets/cards/CardsGrid.vue')['default'] 19 | EditTokenTtl: typeof import('./src/components/namespace/EditTokenTtl.vue')['default'] 20 | GroupDatatable: typeof import('./src/components/group/GroupDatatable.vue')['default'] 21 | 'IAntDesign:saveOutlined': typeof import('~icons/ant-design/save-outlined')['default'] 22 | 'ICi:checkBold': typeof import('~icons/ci/check-bold')['default'] 23 | 'IEosIcons:loading': typeof import('~icons/eos-icons/loading')['default'] 24 | 'IEosIcons:namespace': typeof import('~icons/eos-icons/namespace')['default'] 25 | 'IEp:closeBold': typeof import('~icons/ep/close-bold')['default'] 26 | 'IFaSolid:moon': typeof import('~icons/fa-solid/moon')['default'] 27 | 'IFaSolid:sun': typeof import('~icons/fa-solid/sun')['default'] 28 | 'IFontisto:close': typeof import('~icons/fontisto/close')['default'] 29 | IIonArrowBackOutline: typeof import('~icons/ion/arrow-back-outline')['default'] 30 | 'ILineMd:cancel': typeof import('~icons/line-md/cancel')['default'] 31 | 'IMdi:clockEditOutline': typeof import('~icons/mdi/clock-edit-outline')['default'] 32 | 'IMdi:clockOutline': typeof import('~icons/mdi/clock-outline')['default'] 33 | 'IMdi:logout': typeof import('~icons/mdi/logout')['default'] 34 | LoadingIndicator: typeof import('./src/components/widgets/LoadingIndicator.vue')['default'] 35 | NamespaceDatatable: typeof import('./src/components/namespace/NamespaceDatatable.vue')['default'] 36 | NamespaceInfo: typeof import('./src/components/namespace/NamespaceInfo.vue')['default'] 37 | NamespaceSelector: typeof import('./src/components/namespace/NamespaceSelector.vue')['default'] 38 | OrgDatatable: typeof import('./src/components/org/OrgDatatable.vue')['default'] 39 | RouterLink: typeof import('vue-router')['RouterLink'] 40 | RouterView: typeof import('vue-router')['RouterView'] 41 | SearchForUsers: typeof import('./src/components/admin/add/subviews/SearchForUsers.vue')['default'] 42 | SearchUser: typeof import('./src/components/user/SearchUser.vue')['default'] 43 | SelectUser: typeof import('./src/components/admin/add/subviews/SelectUser.vue')['default'] 44 | SimpleBadge: typeof import('./src/components/widgets/SimpleBadge.vue')['default'] 45 | SimpleCard: typeof import('./src/components/widgets/cards/SimpleCard.vue')['default'] 46 | TheCurrentNamespace: typeof import('./src/components/namespace/TheCurrentNamespace.vue')['default'] 47 | TheLogin: typeof import('./src/components/TheLogin.vue')['default'] 48 | TheSidebar: typeof import('./src/components/TheSidebar.vue')['default'] 49 | TheTopbar: typeof import('./src/components/TheTopbar.vue')['default'] 50 | UserDatatable: typeof import('./src/components/user/UserDatatable.vue')['default'] 51 | UserGroupsInfo: typeof import('./src/components/user/UserGroupsInfo.vue')['default'] 52 | } 53 | } 54 | --------------------------------------------------------------------------------