diff --git a/demo/sites/auditingsite/package.json b/demo/sites/auditingsite/package.json new file mode 100644 index 0000000..4a39ca3 --- /dev/null +++ b/demo/sites/auditingsite/package.json @@ -0,0 +1,29 @@ +{ + "name": "@solidlab/uma-demo-auditing", + "version": "0.1.0", + "private": true, + "dependencies": { + "@comunica/query-sparql": "^2.6.9", + "@inrupt/solid-client-authn-browser": "^1.14.0", + "@mui/material": "^5.15.15", + "n3": "^1.17.3", + "uuid": "^9.0.1" + }, + "scripts": { + "dev": "yarn run -T react-scripts start", + "start": "yarn run -T serve -s build -l 5003", + "build": "yarn run -T react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/demo/sites/auditingsite/public/index.html b/demo/sites/auditingsite/public/index.html new file mode 100644 index 0000000..3348d27 --- /dev/null +++ b/demo/sites/auditingsite/public/index.html @@ -0,0 +1,11 @@ + + + + + Auditing Interface + + + +
+ + diff --git a/demo/sites/auditingsite/public/store.jpg b/demo/sites/auditingsite/public/store.jpg new file mode 100644 index 0000000..3471699 Binary files /dev/null and b/demo/sites/auditingsite/public/store.jpg differ diff --git a/demo/sites/auditingsite/src/App.css b/demo/sites/auditingsite/src/App.css new file mode 100644 index 0000000..7e60831 --- /dev/null +++ b/demo/sites/auditingsite/src/App.css @@ -0,0 +1,85 @@ +.App { + text-align: center; + height: 100vh; + background-color: rgb(235, 235, 235); +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.selected { + background-color: lightblue !important; +} + +.grid-container { + height: 70vh; +} + +#retrievals-listing { +} + +.retrieval-resource-card { + padding: 5px; + margin: 5px; + width: 100%; + word-wrap: break-word; + height: 5em; + text-align: left; +} + +#audit-page { + height: 70vh; +} + +.text-display { + margin: 2em; + overflow-y: scroll; + text-align: left; + white-space: pre-wrap; + height: 80%; + /* todo: fix height problem here other way. */ +} + +#contents { +} + +.left-border { + border-left: 1px solid black; +} + +#store-page { + background-color: #f6f6f6; + padding: 2em; +} \ No newline at end of file diff --git a/demo/sites/auditingsite/src/App.tsx b/demo/sites/auditingsite/src/App.tsx new file mode 100644 index 0000000..7455465 --- /dev/null +++ b/demo/sites/auditingsite/src/App.tsx @@ -0,0 +1,13 @@ + +import './App.css'; + +import ClippedDrawer from "./components/Drawer"; + +export default function App() { + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/demo/sites/auditingsite/src/components/AuditEntryPage.tsx b/demo/sites/auditingsite/src/components/AuditEntryPage.tsx new file mode 100644 index 0000000..1843e3e --- /dev/null +++ b/demo/sites/auditingsite/src/components/AuditEntryPage.tsx @@ -0,0 +1,14 @@ +import { AuditEntry } from "../util/Types"; + +export default function AuditEntryPage({entry}: {entry: AuditEntry}) { + return ( +
+

{entry.resourceId}

+
+

+ {JSON.stringify(entry.contract, null, 2)} +

+
+
+ ) +} diff --git a/demo/sites/auditingsite/src/components/Drawer.tsx b/demo/sites/auditingsite/src/components/Drawer.tsx new file mode 100644 index 0000000..84107e3 --- /dev/null +++ b/demo/sites/auditingsite/src/components/Drawer.tsx @@ -0,0 +1,87 @@ +import { useState, useEffect } from 'react'; +import Box from '@mui/material/Box'; +import Drawer from '@mui/material/Drawer'; +import AppBar from '@mui/material/AppBar'; +import CssBaseline from '@mui/material/CssBaseline'; +import Toolbar from '@mui/material/Toolbar'; +import List from '@mui/material/List'; +import Typography from '@mui/material/Typography'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import StorePage from './StorePage'; +import { Divider } from '@mui/material'; + +const drawerWidth = 250; + +// Map stores on their audits +export interface StoreInfo { + name: string, + site: string, + audit: string, + logo: string, +} + +const stores: StoreInfo[] = [{ + name: "De Buurtwinkel", + site: "http://localhost:5002/", + audit: "http://localhost:5123/audit", + logo: "store.jpg" +}] + +export default function ClippedDrawer() { + + const [selected, setSelected] = useState(undefined) + + return ( + + + theme.zIndex.drawer + 1 }}> + + + Auto Auditing Platform + + + + + + + + + + + + + + + {stores.map((store, index) => ( + setSelected(store)} disablePadding + className={selected && selected?.name === store.name ? "selected" : ""} > + + + + + + + + ))} + + + + + + { + selected && + } + + + ); +} \ No newline at end of file diff --git a/demo/sites/auditingsite/src/components/StorePage.tsx b/demo/sites/auditingsite/src/components/StorePage.tsx new file mode 100644 index 0000000..ab08c9d --- /dev/null +++ b/demo/sites/auditingsite/src/components/StorePage.tsx @@ -0,0 +1,66 @@ +/* eslint-disable max-len */ +import React, { useEffect, useState } from "react"; +import { StoreInfo } from "./Drawer"; +import { Card, Grid, Typography } from "@mui/material"; +import { AuditEntry } from "../util/Types"; +import AuditEntryPage from "./AuditEntryPage"; + + +export default function StorePage({ store }: { store: StoreInfo }) { + + const [auditEntries, setAuditEntries] = useState([]) + const [selectedEntry, setSelectedEntry] = useState(undefined) + + useEffect(() => { + async function fetchAuditEntries() { + const res = await fetch(store.audit) + const parsedEntries = await res.json() as any; + return parsedEntries.map((e: any) => { + e.timestamp = new Date(e.timestamp) + return e + }) + } + fetchAuditEntries().then(entries => setAuditEntries(entries)) //todo: uncomment + + }, []) + + return( +
+

{store.name}

+
+ {/* Data Retrievals */} + + +
+ + {auditEntries.map(entry => + { setSelectedEntry(entry) }}> + + {entry.resourceId} + + + {entry.timestamp.toISOString()} + + + )} + +
+
+ + { + selectedEntry + ? + :
+ } + + + +
+
+ ) + + +} \ No newline at end of file diff --git a/demo/sites/auditingsite/src/index.css b/demo/sites/auditingsite/src/index.css new file mode 100644 index 0000000..b92f6e4 --- /dev/null +++ b/demo/sites/auditingsite/src/index.css @@ -0,0 +1,110 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; + width: 100vw; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +nav { + background-color: lightskyblue; + margin: 0; + height: 3em; +} + +.rowcontainer { + display: flex; + flex-direction: row; +} + +.columncontainer { + display: flex; + flex-direction: column; +} + +.equalcolumns { + flex: 50%; +} + + +#policypage { + height: 80vh; + margin-top: 5vh; +} + +#policymanagementcontainer { + background-color: #fffbe4; + height: 200px; + width: 80vw; + margin: auto; + display: flex; + height: 100%; + overflow: hidden; + border: 1px solid black; +} + +#addPolicy { + flex: 20%; + display: flex; + align-items: center; + justify-content: center; +} + +#addPolicyButton { + height: 60%; + width: 60%; +} + +#policyList { + flex: 80%; + overflow-y: scroll; + padding: 30px; +} + +::-webkit-scrollbar { + width: 0; /* Remove scrollbar space */ + background: transparent; /* Optional: just make scrollbar invisible */ +} +/* Optional: show position indicator in red */ +::-webkit-scrollbar-thumb { + background: #FF0000; +} + +.policyentry { + height: 100px; + border: 2px solid gray; + margin-top: 10px; + background-color:beige; + text-align: left; + border-radius: 1em; + padding: .5em; +} + +.selectedentry { + background-color:lightblue; +} + +#PolicyDisplayScreen { + display: flex; + align-items: center; + justify-content: center; + flex: 60%; +} + + +#policyview { + width: 90%; + height: 90%; +} + +#PolicyListContainer { + flex: 40%; +} diff --git a/demo/sites/auditingsite/src/index.tsx b/demo/sites/auditingsite/src/index.tsx new file mode 100644 index 0000000..dd5b8c5 --- /dev/null +++ b/demo/sites/auditingsite/src/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +root.render( + + + +); + + diff --git a/demo/sites/auditingsite/src/util/Types.ts b/demo/sites/auditingsite/src/util/Types.ts new file mode 100644 index 0000000..c594683 --- /dev/null +++ b/demo/sites/auditingsite/src/util/Types.ts @@ -0,0 +1,37 @@ + +export type ODRLConstraint = { + left: any, + op: any, + right: any, + // leftOperand: any, + // operator: any, + // rightOperand: any, +} + +export type ODRLPermission = { + action: string | string[], + constraint: ODRLConstraint[] +} + +export type Contract = { + "@context": string, + "@type": string, + target: string, // resourceURL + uid: string, // instantiated policy UID + assigner: string, // user WebID + assignee: string, // target WebID + permission: ODRLPermission[], +}; + + +export type Data = string +export type ResourceId = string + +export type AuditEntry = { + contract: Contract, + token: string, + webId: string // todo:: this should not be required + data: Data, + timestamp: Date, + resourceId: ResourceId, +} diff --git a/demo/sites/auditingsite/src/util/Vocabularies.ts b/demo/sites/auditingsite/src/util/Vocabularies.ts new file mode 100644 index 0000000..cf0d14a --- /dev/null +++ b/demo/sites/auditingsite/src/util/Vocabularies.ts @@ -0,0 +1,139 @@ +import { DataFactory } from 'n3'; +import type { NamedNode } from '@rdfjs/types'; + +// shameless copy of the Community Solid Server Vocabularies +/** + * A `Record` in which each value is a concatenation of the baseUrl and its key. + */ +type ExpandedRecord = {[K in TLocal]: `${TBase}${K}` }; + +/** + * Has a base URL as `namespace` value and each key has as value the concatenation with that base URL. + */ +type ValueVocabulary = + { namespace: TBase } & ExpandedRecord; +/** + * A {@link ValueVocabulary} where the URI values are {@link NamedNode}s. + */ +type TermVocabulary = T extends ValueVocabulary ? {[K in keyof T]: NamedNode } : never; + +/** + * Contains a namespace and keys linking to the entries in this namespace. + * The `terms` field contains the same values but as {@link NamedNode} instead of string. + */ +export type Vocabulary = + ValueVocabulary & { terms: TermVocabulary> }; + +/** + * A {@link Vocabulary} where all the non-namespace fields are of unknown value. + * This is a fallback in case {@link createVocabulary} gets called with a non-strict string array. + */ +export type PartialVocabulary = + { namespace: TBase } & + Partial> & + { terms: { namespace: NamedNode } & Partial> }; + +/** + * A local name of a {@link Vocabulary}. + */ +export type VocabularyLocal = T extends Vocabulary ? TKey : never; +/** + * A URI string entry of a {@link Vocabulary}. + */ +export type VocabularyValue = T extends Vocabulary ? T[TKey] : never; +/** + * A {@link NamedNode} entry of a {@link Vocabulary}. + */ +export type VocabularyTerm = T extends Vocabulary ? T['terms'][TKey] : never; + +/** + * Creates a {@link ValueVocabulary} with the given `baseUri` as namespace and all `localNames` as entries. + */ +function createValueVocabulary(baseUri: TBase, localNames: TLocal[]): +ValueVocabulary { + const expanded: Partial> = {}; + // Expose the listed local names as properties + for (const localName of localNames) { + expanded[localName] = `${baseUri}${localName}`; + } + return { + namespace: baseUri, + ...expanded as ExpandedRecord, + }; +} + +/** + * Creates a {@link TermVocabulary} based on the provided {@link ValueVocabulary}. + */ +function createTermVocabulary(values: ValueVocabulary): +TermVocabulary> { + // Need to cast since `fromEntries` typings aren't strict enough + return Object.fromEntries( + Object.entries(values).map(([ key, value ]): [string, NamedNode] => [ key, DataFactory.namedNode(value) ]), + ) as TermVocabulary>; +} + +/** + * Creates a {@link Vocabulary} with the given `baseUri` as namespace and all `localNames` as entries. + * The values are the local names expanded from the given base URI as strings. + * The `terms` field contains all the same values but as {@link NamedNode} instead. + */ +export function createVocabulary(baseUri: TBase, ...localNames: TLocal[]): +string extends TLocal ? PartialVocabulary : Vocabulary { + const values = createValueVocabulary(baseUri, localNames); + return { + ...values, + terms: createTermVocabulary(values), + }; +} + +/** + * Creates a new {@link Vocabulary} that extends an existing one by adding new local names. + * @param vocabulary - The {@link Vocabulary} to extend. + * @param newNames - The new local names that need to be added. + */ +export function extendVocabulary( + vocabulary: Vocabulary, + ...newNames: TNew[] +): + ReturnType> { + const localNames = Object.keys(vocabulary) + .filter((key): boolean => key !== 'terms' && key !== 'namespace') as TLocal[]; + const allNames = [ ...localNames, ...newNames ]; + return createVocabulary(vocabulary.namespace, ...allNames); +} + +export const RDF = createVocabulary( + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'type', + ); + + +export const ODRL = createVocabulary( + 'http://www.w3.org/ns/odrl/2/', + 'Agreement', + 'Offer', + 'Permission', + 'action', + 'target', + 'assignee', + 'assigner', + 'constraint', + 'operator', + 'permission', + 'dateTime', + 'purpose', + 'leftOperand', + 'rightOperand', + 'gt', + 'lt', + 'eq', +) + +export const XSD = createVocabulary( + 'http://www.w3.org/2001/XMLSchema#', + 'dateTime', + 'duration', + 'integer', + 'string', + ); diff --git a/demo/sites/auditingsite/tsconfig.json b/demo/sites/auditingsite/tsconfig.json new file mode 100644 index 0000000..a273b0c --- /dev/null +++ b/demo/sites/auditingsite/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/demo/yarn.lock b/demo/yarn.lock index ec48462..3d20d02 100644 --- a/demo/yarn.lock +++ b/demo/yarn.lock @@ -5063,6 +5063,13 @@ __metadata: languageName: node linkType: hard +"@mui/core-downloads-tracker@npm:^5.15.15": + version: 5.15.15 + resolution: "@mui/core-downloads-tracker@npm:5.15.15" + checksum: 10c0/6bbe71e00b1a1c20984a8b5e3b194cc6114f5eee54b37ac463ecfd47e3b83aacdb500f6f5f201ed24237679fb52b256cc6a7e81d4dcfcf29f2a151852b81d160 + languageName: node + linkType: hard + "@mui/icons-material@npm:^5.15.15": version: 5.15.15 resolution: "@mui/icons-material@npm:5.15.15" @@ -5112,6 +5119,39 @@ __metadata: languageName: node linkType: hard +"@mui/material@npm:^5.15.15": + version: 5.15.15 + resolution: "@mui/material@npm:5.15.15" + dependencies: + "@babel/runtime": "npm:^7.23.9" + "@mui/base": "npm:5.0.0-beta.40" + "@mui/core-downloads-tracker": "npm:^5.15.15" + "@mui/system": "npm:^5.15.15" + "@mui/types": "npm:^7.2.14" + "@mui/utils": "npm:^5.15.14" + "@types/react-transition-group": "npm:^4.4.10" + clsx: "npm:^2.1.0" + csstype: "npm:^3.1.3" + prop-types: "npm:^15.8.1" + react-is: "npm:^18.2.0" + react-transition-group: "npm:^4.4.5" + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 10c0/ed463112556e45ffa6cd1045eb08c412e9b9d80f049be069b557fa41703be437ab9cd7ae0816adf8bcbef73d11f9cd1df2ae82502d4b905fa483642ac01e1b62 + languageName: node + linkType: hard + "@mui/private-theming@npm:^5.15.14": version: 5.15.14 resolution: "@mui/private-theming@npm:5.15.14" @@ -5178,6 +5218,34 @@ __metadata: languageName: node linkType: hard +"@mui/system@npm:^5.15.15": + version: 5.15.15 + resolution: "@mui/system@npm:5.15.15" + dependencies: + "@babel/runtime": "npm:^7.23.9" + "@mui/private-theming": "npm:^5.15.14" + "@mui/styled-engine": "npm:^5.15.14" + "@mui/types": "npm:^7.2.14" + "@mui/utils": "npm:^5.15.14" + clsx: "npm:^2.1.0" + csstype: "npm:^3.1.3" + prop-types: "npm:^15.8.1" + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 10c0/80724377ee9c0e1604373371bb9b7d566c899009264caf6f638eed224600b91b739f32bef03c6a479dc9348dfc01d7c28cc8d1252454d7dd4a23f59033a8653e + languageName: node + linkType: hard + "@mui/types@npm:^7.2.14": version: 7.2.14 resolution: "@mui/types@npm:7.2.14" @@ -5616,6 +5684,7 @@ __metadata: dependencies: "@comunica/query-sparql": "npm:^2.6.9" "@inrupt/solid-client-authn-browser": "npm:^1.14.0" + "@mui/material": "npm:^5.15.15" n3: "npm:^1.17.3" uuid: "npm:^9.0.1" languageName: unknown