Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.*
!.c8rc
!.editorconfig
!.gitignore
!.gitkeep
!.helmignore
Expand All @@ -11,6 +12,5 @@ log*
node_modules
package-lock.json
*.ndjson
*.ppk
*.pub
*.pem
*.tsbuildinfo
9 changes: 0 additions & 9 deletions .prettierrc.json

This file was deleted.

4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# declare image
FROM node:20-alpine as build
FROM node:22-alpine as build

# cd into app dir
WORKDIR /home/app/node
Expand All @@ -16,7 +16,7 @@ RUN npm install \
&& rm -rf src

# declare new (empty) image
FROM node:20-alpine
FROM node:22-alpine

# cd into app dir
WORKDIR /home/app/node
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Released under MIT License

Copyright (c) 2024 Evologi S.r.l.
Copyright (c) 2025 Evologi S.r.l.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
102 changes: 83 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,89 @@ Main API web server for Brer project.

Create a `.env` file with the following envs:

| Name | Description
| ------------------- | -------------------
| NODE_ENV | Must be `"production"` for non-toy envs.
| SERVER_HOST | Listening host. Defaults to `127.0.0.1`.
| SERVER_PORT | Server's post. Defaults to `3000`.
| LOG_LEVEL | [Pino](https://github.com/pinojs/pino) log level. Defaults to `debug`.
| LOG_FILE | Logs filepath. Optional.
| LOG_PRETTY | Set to `"enable"` to pretty-print stdout logs.
| COUCHDB_URL | CouchDB URL. Defaults to `http://127.0.0.1:5984/`.
| COUCHDB_USERNAME |
| COUCHDB_PASSWORD |
| JWT_SECRET | Secred used to sign JWT tokens, may be omitted if `JWT_PRIVATE_KEY` is defined.
| JWT_PRIVATE_KEY | Filepath of a PEM-encoded RSA SHA-256 secret key, may be omitted if `JWT_SECRET` is defined.
| API_PUBLIC_KEY | Filepath of a PEM-encoded RSA SHA-256 public key.
| INVOKER_PUBLIC_KEY | Filepath of a PEM-encoded RSA SHA-256 public key.
| COOKIE_NAME | Defaults to `"brer_session"`.
| COOKIE_DOMAIN | Cookie's domain attribute.
| COOKIE_SECURE | Set to `"enable"` for secure cookies.
| ADMIN_PASSWORD | Set a always-valid `admin` User password.
| Name | Description
| --------------------- | ---------------------
| NODE_ENV | Must be `"production"` for non-toy envs.
| SERVER_HOST | Listening host. Defaults to `127.0.0.1`.
| SERVER_PORT | Server's post. Defaults to `3000`.
| LOG_LEVEL | [Pino](https://github.com/pinojs/pino) log level. Defaults to `debug`.
| LOG_PRETTY | Set to `"enable"` to pretty-print stdout logs.
| LOG_FILE | Logs filepath. Optional.
| LOG_APPEND | Use `"disable"` to trucate the log file at any server restart.
| INVOKER_URL |
| COUCHDB_URL | CouchDB URL. Defaults to `http://127.0.0.1:5984/`.
| COUCHDB_USERNAME |
| COUCHDB_PASSWORD |
| JWT_SECRET | Secred used to sign JWT tokens, may be omitted if `JWT_PRIVATE_KEY` is defined.
| JWT_PRIVATE_KEY | Filepath of a PEM-encoded RSA SHA-256 secret key, may be omitted if `JWT_SECRET` is defined.
| JWT_PUBLIC_KEYS | Filepaths (comma separated) of a PEM-encoded RSA SHA-256 public key.
| JWT_PASSPHRASE |
| COOKIE_NAME_SESSION | Defaults to `"brer_session"`.
| COOKIE_NAME_STATE | Defaults to `"brer_state"`.
| COOKIE_DOMAIN | Cookie's domain attribute.
| COOKIE_SECURE | Set to `"enable"` for secure cookies.
| COOKIE_SECRET |
| ADMIN_USERNAME | Defaults to `"admin"`.
| ADMIN_PASSWORD | Set a always-valid `admin` User password.
| ENTRA_CLIENT_ID |
| ENTRA_CLIENT_SECRET |
| ENTRA_REDIRECT_URI |
| ENTRA_TENANT_ID | Optional.

### Microsoft Entra

The Microsoft Entra integration is optional (you can use `admin` authentication).

#### Setup envs

Go to [Entra dashboard](https://entra.microsoft.com/#home).

Select `Applications`, and then `App registration`.

Click `New registration`.

In `Redirect URI` select `Web` and add `https://my_brer_domain.net/api/session/callback`.

Open the new registration, and select `Overview`.

Copy the `Application (client) ID` into `ENTRA_CLIENT_ID` env.

Copy the `Redirect URI` into `ENTRA_REDIRECT_URI` env.

Open `Certificates and secrets` tab, and click `New client secret`.

Copy the secret value into `ENTRA_CLIENT_SECRET` env.

#### Setup custom Roles

Go to [Entra dashboard](https://entra.microsoft.com/#home).

Click on `Applications`, and then `App registrations`.

Search and open your Brer application.

Click on `App roles`.

Click on `Create app role` and add the following roles (also set `Allowed member types` to `Users/Groups`):

| Display name | Value | Description
| ------------- | ------------- | -------------
| Reader | Brer.Reader | Allow read actions inside the Brer Web UI.
| Invoker | Brer.Invoker | Allow read actions and Functions' invoking inside the Brer Web UI.
| Writer | Brer.Writer | Allow full Functions' management (excluding admin actions).
| Admin | Brer.Admin | Full access to all features, including automation tokens generation.

#### Assign custom Roles

Go to [Entra dashboard](https://entra.microsoft.com/#home).

Click `Applications`, and then `Enterprise applications`.

Search and open your Brer application.

Click `Users and groups`.

Click `Add user/group` and select one or more User and/or Group, and thei custom Role.

### Start

Expand Down
141 changes: 12 additions & 129 deletions bin/init.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node

import HttpAgent, { HttpsAgent } from 'agentkeepalive'
import { HttpAgent, HttpsAgent } from 'agentkeepalive'
import minimist from 'minimist'
import nano from 'nano'
import { read } from 'read'
Expand All @@ -12,144 +12,27 @@ const couch = await createServerScope()
console.log('test couchdb connection')
await couch.info()

const dbFunctions = couch.scope('functions')
const dbInvocations = couch.scope('invocations')
const dbScopes = couch.scope('scopes')
const dbUsers = couch.scope('users')

console.log('init databases')
await Promise.all([
pushDatabase(dbFunctions.config.db),
pushDatabase(dbInvocations.config.db),
pushDatabase(dbScopes.config.db),
pushDatabase(dbUsers.config.db),
])

const reduceArrays = `
function (keys, values, rereduce) {
return values.reduce((a, b) => a.concat(b), [])
}
`
const db = couch.scope('functions')

console.log('init database')
await pushDatabase(db.config.db)

const listFunctions = `
function (doc) {
emit([doc.name], null)
}
`

const listFunctionsByProject = `
function (doc) {
emit([doc.project, doc.name], null)
}
`

console.log('write functions views')
await pushDocument(dbFunctions, {
_id: '_design/default',
await pushDocument(db, {
_id: '_design/api',
views: {
list: {
map: listFunctions,
},
list_by_project: {
map: listFunctionsByProject,
},
},
})

const invocationPriority = `
var priority = -1
if (doc.runtimeTest) {
if (doc.status !== 'completed' && doc.status !== 'failed') {
priority = 2
}
} else if (doc.status === 'completed' || doc.status === 'failed') {
priority = 0
} else {
priority = 1
}
`

const listInvocations = `
function (doc) {
${invocationPriority}

if (priority >= 0) {
emit([priority, doc.createdAt], null)
}
}
`

const listInvocationsByFunction = `
function (doc) {
${invocationPriority}

if (priority >= 0) {
emit([doc.functionName, priority, doc.createdAt], null)
}
}
`

const listInvocationsByProject = `
function (doc) {
${invocationPriority}

if (priority >= 0) {
emit([doc.project, priority, doc.createdAt], null)
}
}
`

const mapInvocationsByIdempotencyKey = `
function (doc) {
if (
doc.idempotencyKey &&
(doc.status === 'pending' || doc.status === 'initializing')
) {
emit(doc.idempotencyKey, null)
}
}
`

console.log('write invocations views')
await pushDocument(dbInvocations, {
_id: '_design/default',
views: {
list: {
map: listInvocations,
},
list_by_function: {
map: listInvocationsByFunction,
},
list_by_project: {
map: listInvocationsByProject,
},
},
})

console.log('write default scopes')
await Promise.all([
pushDocument(dbScopes, {
_id: 'brer.io/scope/reader',
name: 'reader',
role: 'reader',
}),
pushDocument(dbScopes, {
_id: 'brer.io/scope/invoker',
name: 'invoker',
role: 'invoker',
}),
pushDocument(dbScopes, {
_id: 'brer.io/scope/writer',
name: 'writer',
role: 'writer',
}),
pushDocument(dbScopes, {
_id: 'brer.io/scope/admin',
name: 'admin',
admin: true,
}),
])

console.log('all done')

/**
Expand All @@ -171,19 +54,19 @@ async function ask(options) {
async function createServerScope() {
const url = await ask({
prompt: 'couchdb url: ',
default: args['couchdb-url'] || 'http://127.0.0.1:5984/',
default: args['couch-url'] || 'http://127.0.0.1:5984/',
edit: true,
})

const username = await ask({
prompt: 'couchdb username: ',
default: args['couchdb-username'] || 'admin',
default: args['couch-username'] || 'brer',
edit: true,
})

const password = await ask({
prompt: 'couchdb password: ',
default: args['couchdb-password'],
default: args['couch-password'],
silent: true,
replace: '*',
})
Expand Down Expand Up @@ -220,8 +103,8 @@ async function pushDatabase(databaseName) {
*/
async function pushDocument(scope, doc) {
try {
const result = await scope.get(doc._id)
doc = Object.assign(result, doc)
const current = await scope.get(doc._id)
doc._rev = current._rev
} catch (err) {
if (Object(err).statusCode !== 404) {
return Promise.reject(err)
Expand Down
Loading