Skip to content

Commit

Permalink
feat!: implement a happy path to join a group
Browse files Browse the repository at this point in the history
Based on https://github.com/solidcouch/geoindex
and diverges here.
  • Loading branch information
mrkvon committed Jan 6, 2025
1 parent fc2a7fa commit 4e35e82
Show file tree
Hide file tree
Showing 35 changed files with 320 additions and 2,464 deletions.
16 changes: 0 additions & 16 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,3 @@ PORT=

## set to 1 if the app is running behind reverse proxy
#BEHIND_PROXY=

# comma-separated uris of groups that are indexed by the service (required)
INDEXED_GROUPS=

## comma-separated uris of groups that are allowed to query the service - defaults to INDEXED_GROUPS when undefined. Uncomment (set to empty string) to allow everybody.
#ALLOWED_GROUPS=

## configure types of things we're interested in
THING_TYPE="http://w3id.org/hospex/ns#Accommodation"

## cron-style schedule to define how often the index should refresh
## e.g. the example below makes the index run every 6 hours (default)
#REFRESH_SCHEDULE="0 */6 * * *"

## TODO configure discovery of the thing from the person's profile, probably as ldhop query JSON
## defaults to whatever is needed for it to run with SolidCouch
22 changes: 0 additions & 22 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,24 +1,2 @@
## port on which the service will run (required)
PORT=0

## base url of the service - defaults to localhost with the given port
#BASE_URL=

## set to 1 if the app is running behind reverse proxy
#BEHIND_PROXY=

# comma-separated uris of groups that are indexed by the service (required)
INDEXED_GROUPS=_

## comma-separated uris of groups that are allowed to query the service - defaults to INDEXED_GROUPS when undefined. Uncomment (set to empty string) to allow everybody.
#ALLOWED_GROUPS=

## configure types of things we're interested in
THING_TYPE="http://w3id.org/hospex/ns#Accommodation"

## cron-style schedule to define how often the index should refresh
## e.g. the example below makes the index run every 6 hours (default)
#REFRESH_SCHEDULE="0 */6 * * *"

## TODO configure discovery of the thing from the person's profile, probably as ldhop query JSON
## defaults to whatever is needed for it to run with SolidCouch
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Implement a happy path for joining

### Changed

- Based on [geoindex service](https://github.com/solidcouch/geoindex)
23 changes: 9 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Solid Geoindex
# Solid Community Inbox

This service keeps track of Things of specific type that have a location on Earth and belong to defined groups of people.
This service accepts Join and Leave activity sent by an authenticated person, validates the request, and adds or removes them from the given community accordingly. The inbox can be linked from community's URI by [`ldp:inbox`](http://www.w3.org/ns/ldp#inbox) predicate. It may also do some other membership tasks (or not).

## How it works

- It's a bot agent with its own identity.
- It's an agent with its own webId, and it needs read and write access to the community Solid pod.
- It runs on a server.
- When you create, update or remove a Thing, you send a notification to this service's inbox. The service will fetch and save the Thing's uri and location.
- The service regularly crawls Things of its group members, and updates itself accordingly. (In case it missed a notification.)
- The group members can query the service for Things at certain geohash, using Triple Pattern Fragment (not fully compatible, yet).
- It has an `/inbox` endpoint.
- When you POST a `Join` activity to the inbox, it validates that agent, object, and authenticated person all match; and possibly check other conditions. (Invite only, temporary joining, ...). If conditions are met, it adds the person's webId to one of the community groups.
- When you POST a `Leave` activity to the inbox, it validates that agent, object, and authenticated person all match, and remove the person from the community groups.
- It may also support invite-only communities, or admin reviews.
- It may also regularly remove members who haven't validated their email (or not)

## Usage

Expand Down Expand Up @@ -58,14 +60,7 @@ Tests are placed in [src/test/](./src/test/)

## TODO

- [ ] remove stale accommodations after index updates, otherwise they may hang there forever
- [ ] maybe also validate and store the person, community, ... in the database. We may have index for multiple communities in the future.
- [ ] maybe configure the server by providing its webId - that will provide baseUrl, hash, path to webId.
- [ ] cache the groups, possibly with etags - they don't need to be fetched every time.

## Maybe

- [ ] index multiple communities, and show the results to particular community members only
TODO

## License

Expand Down
3 changes: 0 additions & 3 deletions TODO.md

This file was deleted.

19 changes: 0 additions & 19 deletions apidocs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,6 @@
}
}
}
},
"/query": {
"get": {
"description": "",
"parameters": [
{
"name": "object",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"default": {
"description": ""
}
}
}
}
},
"components": {
Expand Down
3 changes: 1 addition & 2 deletions knip.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"project": ["**/*.{js,ts}"],
"entry": ["src/index.ts", "src/generate-api-docs.ts"],
"ignore": ["apidocs/*"],
"ignoreDependencies": ["sqlite3"]
"ignore": ["apidocs/*"]
}
13 changes: 2 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "geoindex",
"version": "0.0.1",
"name": "solid-community-inbox",
"version": "0.0.0",
"license": "MIT",
"type": "module",
"main": "dist/index.js",
Expand Down Expand Up @@ -31,33 +31,24 @@
"@koa/bodyparser": "^5.1.1",
"@koa/cors": "^5.0.0",
"@koa/router": "^13.1.0",
"@ldhop/core": "^0.0.1-alpha.15",
"@soid/koa": "^0.1.2",
"@solid/access-token-verifier": "^2.0.5",
"@types/koa": "^2.15.0",
"@types/koa-static": "^4.0.4",
"@types/koa__cors": "^5.0.0",
"@types/koa__router": "^12.0.4",
"@types/lodash": "^4.17.14",
"@types/n3": "^1.16.0",
"@types/ngeohash": "^0.6.8",
"@types/node": "^22.10.5",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"cron": "^3.3.2",
"css-authn": "^0.0.16",
"dotenv": "^16.4.7",
"helmet": "^8.0.0",
"koa": "^2.15.3",
"koa-helmet": "^8.0.1",
"koa-static": "^5.0.0",
"lodash": "^4.17.21",
"n3": "^1.23.1",
"ngeohash": "^0.6.3",
"rdf-namespaces": "^1.11.0",
"sequelize": "^6.37.5",
"sqlite3": "^5.1.7",
"type-fest": "^4.31.0",
"typescript": "^5.7.2"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
Expand Down
46 changes: 16 additions & 30 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,29 @@ import { solidIdentity } from '@soid/koa'
import Koa from 'koa'
import helmet from 'koa-helmet'
import serve from 'koa-static'
import {
fetchThing,
saveThing,
validateThing,
} from './controllers/processThing.js'
import { queryThings } from './controllers/query.js'
import { initializeDatabase } from './database.js'
import { authorizeGroups } from './middlewares/authorizeGroup.js'
import { type AppConfig, loadConfig } from './middlewares/loadConfig.js'
import { addPersonToGroup } from './controllers/inbox.js'
import { loadConfig } from './middlewares/loadConfig.js'
import { solidAuth } from './middlewares/solidAuth.js'
import { validateBody } from './middlewares/validate.js'
import * as schema from './schema.js'

export interface AppConfig {
readonly webId: string
readonly isBehindProxy?: boolean
readonly joinGroup: string
}

const createApp = async (config: AppConfig) => {
const identity = solidIdentity(config.webId)

await initializeDatabase(config.database)

const app = new Koa()
app.proxy = config.isBehindProxy
app.proxy = Boolean(config.isBehindProxy)
const router = new Router<{ config: AppConfig } & { user: string }>()

router
.use(identity.routes())
.post(
'/inbox',
solidAuth,
authorizeGroups(config.allowedGroups),
/* #swagger.requestBody = {
router.use(identity.routes()).post(
'/inbox',
solidAuth,
/* #swagger.requestBody = {
required: true,
content: {
'application/ld+json': {
Expand All @@ -44,17 +38,9 @@ const createApp = async (config: AppConfig) => {
},
}
*/
validateBody(schema.notification),
fetchThing,
validateThing,
saveThing,
)
.get(
'/query',
solidAuth,
authorizeGroups(config.allowedGroups),
queryThings,
)
validateBody(schema.notification),
addPersonToGroup(config.joinGroup),
)

app
.use(helmet.default())
Expand Down
8 changes: 4 additions & 4 deletions src/config/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const stringToBoolean = (value: string | undefined): boolean => {
return !!value
}

export const stringToArray = (value: string) => {
if (!value) return []
return value.split(/\s*,\s*/)
}
// export const stringToArray = (value: string) => {
// if (!value) return []
// return value.split(/\s*,\s*/)
// }
34 changes: 6 additions & 28 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { Options } from 'sequelize'
import { hospex } from '../namespaces.js'
import { ConfigError } from '../utils/errors.js'
import { stringToArray, stringToBoolean } from './helpers.js'
import { stringToBoolean } from './helpers.js'

// define environment variables via .env file, or via environment variables directly (depends on your setup)
// define environment variables via .env file, or via environment variables directly

if (!process.env.PORT || isNaN(Number(process.env.PORT))) {
throw new ConfigError(
'Please specify the PORT at which the app should run in .env or environment variables',
)
throw new ConfigError('Please specify PORT in environment variables.')
}
export const port = +process.env.PORT

Expand All @@ -22,27 +18,9 @@ export const webId = new URL('/profile/card#bot', baseUrl).toString()

export const isBehindProxy = stringToBoolean(process.env.BEHIND_PROXY)

// indexed groups are required
if (!process.env.INDEXED_GROUPS) {
if (!process.env.GROUP_TO_JOIN)
throw new ConfigError(
'Please specify comma-separated list of INDEXED_GROUPS in .env or environment variables',
'Please specify GROUP_TO_JOIN in environment variables.',
)
}

export const indexedGroups = stringToArray(process.env.INDEXED_GROUPS)

export const allowedGroups =
typeof process.env.ALLOWED_GROUPS === 'undefined'
? indexedGroups
: stringToArray(process.env.ALLOWED_GROUPS)

export const thingTypes = stringToArray(
process.env.THING_TYPES ?? hospex + 'Accommodation',
)

export const refreshSchedule = process.env.REFRESH_SCHEDULE ?? '0 */6 * * *'

export const database: Options = {
dialect: 'sqlite',
storage: undefined,
}
export const joinGroup = process.env.GROUP_TO_JOIN
29 changes: 29 additions & 0 deletions src/controllers/inbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getAuthenticatedFetch } from '@soid/koa'
import { type Middleware } from 'koa'
import assert from 'node:assert'
import { solid, vcard } from 'rdf-namespaces'
import { AppConfig } from '../app.js'
import { HttpError } from '../utils/errors.js'

export const addPersonToGroup =
(group: string): Middleware<{ user: string; config: AppConfig }> =>
async ctx => {
const authFetch = await getAuthenticatedFetch(ctx.state.config.webId)
const response = await authFetch(group, {
headers: { 'content-type': 'text/n3' },
method: 'PATCH',
body: `_:add a <${solid.InsertDeletePatch}>;
<${solid.inserts}> { <${group}> <${vcard.hasMember}> <${ctx.state.user}> . }.
`,
})

if (!response.ok)
throw new HttpError(
`Adding person ${ctx.state.user} to group ${group} failed.`,
response,
)

assert(response.ok)

ctx.status = 200
}
Loading

0 comments on commit 4e35e82

Please sign in to comment.