Reorganize docs.

This commit is contained in:
Lauri Ojansivu 2024-06-27 14:01:26 +03:00
parent ea5d0999c4
commit 057ac4031e
22 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,8 @@
# v1.0 2022-03-22 Perl Asana export to WeKan ® release
This release adds the following new features:
- [Added Perl scripts for Asana export to WeKan ®](https://github.com/wekan/wekan/commit/376bcbb373d16317060adc2b1154cc20496775cc).
Thanks to GeekRuthie.
Thanks to above GitHub users for their contributions and translators for their translations.

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022 WeKan ® Team and GeekRuthie
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,178 @@
#!/usr/local/perl-cbt/bin/perl
use Modern::Perl;
use Carp;
use Data::Dumper;
use HTTP::Request;
use JSON;
use LWP::UserAgent;
use MIME::Base64 qw/encode_base64/;
my $BASE_URL = 'https://app.asana.com/api/1.0';
my $ASANA_API_KEY = 'ASANA_PERSONAL_TOKEN';
my $ua = LWP::UserAgent->new();
open my $input_wekan, '<', 'template.json';
my $wekan_json = readline($input_wekan);
close $input_wekan;
my $wekan_board = decode_json($wekan_json);
my %users;
my %users_by_gid;
# get user IDs from template board
foreach my $user ( @{ $wekan_board->{users} } ) {
$users{ $user->{profile}->{fullname} } = $user->{_id};
}
# get list IDs from template (we ended up not using these)
my %lists;
foreach my $list ( @{ $wekan_board->{lists} } ) {
$lists{ $list->{title} } = $list->{_id};
}
my @headers;
push @headers, ( 'Accept', 'application/json' );
push @headers, ( 'Authorization', "Bearer $ASANA_API_KEY" );
my $projects_req = HTTP::Request->new( "GET", "$BASE_URL/projects", \@headers, );
my $projects_res = $ua->request($projects_req);
my $projects = decode_json( $projects_res->content )->{data};
foreach my $project (@$projects) {
say "Project: ".$project->{name};
my $tasks_url =
'/tasks?project='
. $project->{gid}
. '&opt_fields=completed,name,notes,assignee,created_by,memberships.project.name, memberships.section.name,due_on,created_at,custom_fields';
my $tasks_req = HTTP::Request->new( 'GET', "$BASE_URL$tasks_url", \@headers );
my $tasks_res = $ua->request($tasks_req);
my @output_tasks;
my $tasks = decode_json( $tasks_res->content )->{data};
foreach my $task (@$tasks) {
next if $task->{completed};
say ' - '.$task->{name};
my $git_branch;
my $prio;
foreach my $custom ( @{ $task->{custom_fields} } ) {
if ( $custom->{name} eq 'git branch' ) {
$git_branch = $custom->{text_value};
next;
}
# We ended up not importing these.
if ( $custom->{name} eq 'Priority' && defined $custom->{display_value} ) {
$prio =
$custom->{display_value} eq 'High' ? 'fwccC9'
: $custom->{display_value} eq 'Med' ? 'yPnaFa'
: $custom->{display_value} eq 'Low' ? 'W4vMvm'
: 'ML5drH';
next;
}
}
if ( !defined $users_by_gid{ $task->{created_by}->{gid} } ) {
my $user_req =
HTTP::Request->new( 'GET', "$BASE_URL/users/" . $task->{created_by}->{gid},
\@headers );
my $user_res = $ua->request($user_req);
my $user = decode_json( $user_res->content )->{data};
if ( defined $users{ $user->{name} } ) {
$users_by_gid{ $task->{created_by}->{gid} } = $users{ $user->{name} };
}
}
my $creator = $users_by_gid{ $task->{created_by}->{gid} } // undef;
if ( defined $task->{assignee} && !defined $users_by_gid{ $task->{assignee}->{gid} } ) {
my $user_req =
HTTP::Request->new( 'GET', "$BASE_URL/users/" . $task->{assignee}->{gid},
\@headers );
my $user_res = $ua->request($user_req);
my $user = decode_json( $user_res->content )->{data};
if ( defined $users{ $user->{name} } ) {
$users_by_gid{ $task->{assignee}->{gid} } = $users{ $user->{name} };
}
}
my $assignee = defined $task->{assignee} ? $users_by_gid{ $task->{assignee}->{gid} } : undef;
my $list;
foreach my $membership ( @{ $task->{memberships} } ) {
next if $membership->{project}->{name} ne $project->{name};
$list = $membership->{section}->{name};
}
# I was trying to create JSON that I could use on the import screen in Wekan,
# but for bigger boards, it was just *too* hefty, so I took that JSON and used
# APIs to import.
my %output_task = (
swimlaneId => 'As4SNerx4Y4mMnJ8n', # 'Bugs'
sort => 0,
type => 'cardType-card',
archived => JSON::false,
title => $task->{name},
description => $task->{notes},
createdAt => $task->{created_at},
dueAt => defined $task->{due_on} ? $task->{due_on} . 'T22:00:00.000Z' : undef,
customFields => [
{
_id => 'rL8BpFHp5xxSFbDdr',
value => $git_branch,
},
],
labelIds => [$prio],
listId => $list,
userId => $creator,
assignees => [$assignee],
);
my @final_comments;
my $comments_req =
HTTP::Request->new( 'GET', "$BASE_URL/tasks/" . $task->{gid} . '/stories',
\@headers );
my $comments_res = $ua->request($comments_req);
my $comments = decode_json( $comments_res->content )->{data};
foreach my $comment (@$comments) {
next if $comment->{type} ne 'comment';
if ( !defined $users_by_gid{ $comment->{created_by}->{gid} } ) {
my $user_req =
HTTP::Request->new( 'GET', "$BASE_URL/users/" . $comment->{created_by}->{gid},
\@headers );
my $user_res = $ua->request($user_req);
my $user = decode_json( $user_res->content )->{data};
if ( defined $users{ $user->{name} } ) {
$users_by_gid{ $comment->{created_bye}->{gid} } = $users{ $user->{name} };
}
}
my $commentor = $users_by_gid{ $comment->{created_by}->{gid} };
my %this_comment = (
text => $comment->{text},
createdAt => $comment->{created_at},
userId => $commentor,
);
push @final_comments, \%this_comment;
}
$output_task{comments} = \@final_comments;
my @final_attachments;
my $attachments_req =
HTTP::Request->new( 'GET', "$BASE_URL/tasks/" . $task->{gid} . '/attachments',
\@headers );
my $attachments_res = $ua->request($attachments_req);
my $attachments = decode_json( $attachments_res->content )->{data};
foreach my $attachment (@$attachments) {
my $att_req =
HTTP::Request->new( 'GET', "$BASE_URL/attachments/" . $attachment->{gid},
\@headers );
my $att_res = $ua->request($att_req);
my $att = decode_json( $att_res->content )->{data};
my $file_req=HTTP::Request->new('GET',$att->{download_url});
my $file_res=$ua->request($file_req);
my $file=encode_base64($file_res->content);
my %this_attachment = (
file => $file,
name => $att->{name},
createdAt => $att->{created_at},
);
push @final_attachments, \%this_attachment;
}
$output_task{attachments} = \@final_attachments;
push @output_tasks, \%output_task;
}
my $file_name = $project->{name};
$file_name =~ s/\//_/g;
open my $output_file, '>',$file_name.'_exported.json';
print $output_file encode_json(\@output_tasks);
close $output_file;
}

View file

@ -0,0 +1,122 @@
#!/usr/local/perl-cbt/bin/perl
use Modern::Perl;
use Carp;
use Data::Dumper;
use HTTP::Request;
use JSON;
use LWP::UserAgent;
use MIME::Base64 qw/decode_base64/;
use Try::Tiny;
my $BASE_URL = 'https://taskboard.example.com/api';
my $TOKEN = 'MY_TOKEN';
my $me = 'MY_USER_ID';
my $ua = LWP::UserAgent->new();
my @headers;
push @headers, ( 'Accept', 'application/json' );
push @headers, ( 'Authorization', "Bearer $TOKEN" );
my @form_headers;
push @form_headers, ( 'Content-Type', 'application/json' );
push @form_headers, ( 'Accept', 'application/json' );
push @form_headers, ( 'Authorization', "Bearer $TOKEN" );
# Prior to running this, I built all the boards, with labels and lists and swimlanes.
# Grabbed the IDs for the boards for each project, and put them here to match up with
# filenames from the Asana export script.
my %board_to_use = (
Project_1 => 'F5ZiCXnf4d7qNBRjp',
Project_2 => 'Shw3tyfC2JWCutBLj',
);
opendir my $files_dir, '.'
or croak "Cannot open input directory: $!";
my @files = readdir $files_dir;
closedir $files_dir;
foreach my $tasks_file (@files) {
next if $tasks_file !~ /_exported.json/;
my $project = $tasks_file;
$project =~ s/_exported.json//;
say "Project - $project";
my $board;
if ( $board_to_use{$project} ) {
$board = $board_to_use{$project};
}
say ' No board!' if !$board;
next if !$board;
my $labels_req = HTTP::Request->new( 'GET', "$BASE_URL/boards/$board", \@headers );
my $labels_res = $ua->request($labels_req);
my $board_data = decode_json( $labels_res->content);
my $labels = $board_data->{labels};
my $label_to_use;
# We're merging several Asana boards onto one Wekan board, with labels per project.
foreach my $label (@$labels) {
$label_to_use = $label->{_id} if $label->{name} eq $project;
}
my $lanes_req = HTTP::Request->new( 'GET', "$BASE_URL/boards/$board/swimlanes", \@headers );
my $lanes_res = $ua->request($lanes_req);
my $lanes = decode_json( $lanes_res->content );
my $lane_to_use;
foreach my $lane (@$lanes) {
# Our Asana didn't use swimlanes; all of our Wekan boards have a "Bugs" lane, so use that.
$lane_to_use = $lane->{_id} if $lane->{title} eq 'Bugs';
}
my $lists_req = HTTP::Request->new( 'GET', "$BASE_URL/boards/$board/lists", \@headers );
my $lists_res = $ua->request($lists_req);
my $lists = decode_json( $lists_res->content );
my %list_to_use;
foreach my $list (@$lists) {
$list_to_use{ $list->{title} } = $list->{_id};
}
open my $task_export_file, '<', $tasks_file;
my $tasks_json = readline($task_export_file);
close $task_export_file;
my $tasks = decode_json($tasks_json);
foreach my $task (@$tasks) {
say ' - ' . $task->{title};
my %body_info = (
swimlaneId => $lane_to_use,
authorId => $task->{userId},
assignees => $task->{assignees},
title => $task->{title},
description => $task->{description},
);
my $body = encode_json( \%body_info );
my $list = $list_to_use{ $task->{listId} } // $list_to_use{'Backlog'};
my $task_req = HTTP::Request->new( 'POST', "$BASE_URL/boards/$board/lists/$list/cards",
\@form_headers, $body );
my $task_res = $ua->request($task_req);
my $res;
try {
$res = decode_json( $task_res->content );
} catch {
# Did these manually afterward.
say "--->UNABLE TO LOAD TASK";
next;
};
my $card = $res->{_id};
if ($label_to_use) {
my $card_edit_body = encode_json( { labelIds => [ $label_to_use ]});
my $card_edit_req = HTTP::Request->new( 'PUT', "$BASE_URL/boards/$board/lists/$list/cards/$card",
\@form_headers, $card_edit_body );
my $card_edit_res = $ua->request($card_edit_req);
}
foreach my $comment ( @{ $task->{comments} } ) {
my $comment_body =
encode_json( { authorId => $comment->{userId}, comment => $comment->{text} } );
my $comment_req =
HTTP::Request->new( 'POST', "$BASE_URL/boards/$board/cards/$card/comments",
\@form_headers, $comment_body );
my $comment_res = $ua->request($comment_req);
}
}
}

View file

@ -0,0 +1,9 @@
# v1.0 2021-12-26 WeKan ® Python Trello API CLI release
This release adds the following new features:
- [Added api.py for using newest Trello API, to show Trello boards/cards/actions/reactions JSON and download Trello attachments
as binary files from S3](https://github.com/wekan/wekan/commit/aff6e361f03f1a7e269edc184884313557c94362).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021-2022 The Wekan Team
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

180
docs/ImportExport/trello/api.py Executable file
View file

@ -0,0 +1,180 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# vi:ts=4:et
# Trello API Python CLI
# License: MIT / WeKan Team
try:
# python 3
from urllib.parse import urlencode
from urllib.request import urlretrieve
except ImportError:
# python 2
from urllib import urlencode
import json
import requests
import sys
# ------- TODO START -------------
#
# - Check nested resources about how to recursively get all reactins etc:
# https://developer.atlassian.com/cloud/trello/guides/rest-api/nested-resources/
# - Add checking status codes and stop/delay if errors in API.
# If board is big, instead get small amount of board with paging of Trello REST API,
# then have small delay, and then get more of that big amount of data, so that
# there would not be timeouts with too much data
# https://developer.atlassian.com/cloud/trello/guides/rest-api/status-codes/
# - Add batch requests, to get enough data at once:
# https://developer.atlassian.com/cloud/trello/rest/api-group-batch/#api-batch-get
# - Add rate limits with delays:
# https://developer.atlassian.com/cloud/trello/guides/rest-api/rate-limits/
# - Use webhooks to receive data from Trello to WeKan, so that there would not be
# need to repeatedly get same data again (no polling data), but instead get
# changes pushed to WeKan with webhooks when they happen
# https://developer.atlassian.com/cloud/trello/guides/rest-api/webhooks/
# https://developer.atlassian.com/cloud/trello/rest/api-group-webhooks/#api-webhooks-post
#
# ------- TODO END -------------
# ------- TRELLO SETTINGS START -------------
#
# READ ABOVE TODO FIRST, BE CAREFUL WITH RATE LIMITS ETC.
#
# Keys and tokens:
# - See API introduction:
# https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/
# - Get developer API key and create token at top of https://trello.com/app-key
#
key = 'TRELLO-API-KEY-HERE'
token = 'TRELLO-API-TOKEN-HERE'
#
# ------- TRELLO SETTINGS END -------------
arguments = len(sys.argv) - 1
if arguments == 0:
print("=== Trello API Python CLI ===")
print("License: MIT / WeKan Team")
print("See settings in this api.py script for api key and token.")
print("If *nix: chmod +x api.py => ./api.py users")
print("Syntax:")
print(" python3 api.py emoji # List all available emoji")
print(" python3 api.py boards # List All Boards")
print(" python3 api.py board BOARDID # Info of BOARDID")
print(" python3 api.py card CARDID # Info of CARDID")
print(" python3 api.py actions BOARDID # Actions of BOARDID")
print(" python3 api.py reactions ACTIONID # Reactions of ACTIONID")
print(" python3 api.py attachments CARDID # List attachments of CARDID")
print(" python3 api.py download ATTACHMENTURL # Download file from attachment URL like https://.../image.png with streaming and minimal RAM usage")
exit
if arguments == 2:
if sys.argv[1] == 'board':
# ------- BOARD START -----------
#headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
headers = {'Accept': 'application/json'}
boardid = sys.argv[2]
print("=== ONE BOARD ===\n")
listboard = 'https://api.trello.com/1/boards/' + boardid + '?key=' + key + '&token=' + token
body = requests.get(listboard, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- BOARD END -----------
if sys.argv[1] == 'card':
# ------- CARD START -----------
headers = {'Accept': 'application/json'}
cardid = sys.argv[2]
print("=== ONE CARD ===\n")
listcard = 'https://api.trello.com/1/cards/' + cardid + '?fields=all&key=' + key + '&token=' + token
body = requests.get(listcard, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- BOARD END -----------
if sys.argv[1] == 'actions':
# ------- BOARD ACTIONS START -----------
headers = {'Accept': 'application/json'}
boardid = sys.argv[2]
print("=== ONE BOARD ACTIONS ===\n")
listboardactions = 'https://api.trello.com/1/boards/' + boardid + '/actions?key=' + key + '&token=' + token
body = requests.get(listboardactions, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- BOARD ACTIONS END -----------
if sys.argv[1] == 'reactions':
# ------- REACTIONS OF ACTIONID START -----------
headers = {'Accept': 'application/json'}
actionid = sys.argv[2]
print("=== REACTIONS OF ACTIONID ===\n")
listreactions = 'https://api.trello.com/1/actions/' + actionid + '/reactionsSummary?key=' + key + '&token=' + token
body = requests.get(listreactions, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- REACTIONS OF ACTIONID END -----------
if sys.argv[1] == 'attachments':
# ------- LIST CARD ATTACHMENTS START -----------
headers = {'Accept': 'application/json'}
cardid = sys.argv[2]
print("=== LIST CARD ATTACHMENTS ===\n")
listcardattachments = 'https://api.trello.com/1/cards/' + cardid + '/attachments?key=' + key + '&token=' + token
body = requests.get(listcardattachments, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- LIST CARD ATTACHMENTS END -----------
if sys.argv[1] == 'download':
# ------- DOWNLOAD BOARD ATTACHMENT START -----------
headers = {'Accept': 'application/json', 'Authorization': 'OAuth oauth_consumer_key="' + key + '", oauth_token="' + token + '"'}
url = sys.argv[2]
print("=== DOWNLOAD BOARD ATTACHMENT ===\n")
local_filename = url.split('/')[-1]
# NOTE the stream=True parameter below. Does streaming download with minimal RAM usage.
with requests.get(url, stream=True, headers=headers) as r:
r.raise_for_status()
with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
# If you have chunk encoded response uncomment if
# and set chunk_size parameter to None.
#if chunk:
f.write(chunk)
print("filename: " + local_filename + "\n")
# ------- DOWNLOAD BOARD ATTACHMENT END -----------
if arguments == 1:
if sys.argv[1] == 'boards':
# ------- LIST OF BOARDS START -----------
headers = {'Accept': 'application/json'}
print("=== BOARDS ===\n")
listboards = 'https://api.trello.com/1/members/me/boards?key=' + key + '&token=' + token
body = requests.get(listboards, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- LIST OF BOARDS END -----------
if sys.argv[1] == 'emoji':
# ------- LIST OF EMOJI START -----------
headers = {'Accept': 'application/json'}
print("=== LIST OF ALL EMOJI ===\n")
listemoji = 'https://api.trello.com/1/emoji?key=' + key + '&token=' + token
body = requests.get(listemoji, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- LIST OF EMOJI END -----------

File diff suppressed because it is too large Load diff

461
docs/Login/ldap-sync/ldap-sync.py Executable file
View file

@ -0,0 +1,461 @@
#!/usr/bin/env python3
# ChangeLog
# ---------
# 2022-10-29:
# LDAP sync script added, thanks to hpvb:
# - syncs LDAP teams and avatars to WeKan MongoDB database
# - removes or disables WeKan users that are also disabled at LDAP
# TODO:
# - There is hardcoded value of avatar URL example.com .
# Try to change it to use existing environment variables.
import os
import environs
import ldap
import hashlib
from pymongo import MongoClient
from pymongo.errors import DuplicateKeyError
env = environs.Env()
stats = {
'created': 0,
'updated': 0,
'disabled': 0,
'team_created': 0,
'team_updated': 0,
'team_disabled': 0,
'team_membership_update': 0,
'board_membership_update': 0
}
mongodb_client = MongoClient(env('MONGO_URL'))
mongodb_database = mongodb_client[env('MONGO_DBNAME')]
class LdapConnection:
def __init__(self):
self.url = env('LDAP_URL')
self.binddn = env('LDAP_BINDDN', default='')
self.bindpassword = env('LDAP_BINDPASSWORD', default='')
self.basedn = env('LDAP_BASEDN')
self.group_base = env('LDAP_GROUP_BASE')
self.group_name_attribute = env('LDAP_GROUP_NAME_ATTRIBUTE')
self.admin_group = env('LDAP_ADMIN_GROUP', default=None)
self.user_base = env('LDAP_USER_BASE')
self.user_group = env('LDAP_USER_GROUP', default=None)
self.user_objectclass = env('LDAP_USER_OBJECTCLASS')
self.user_username_attribute = env('LDAP_USER_USERNAME_ATTRIBUTE')
self.user_fullname_attribute = env('LDAP_USER_FULLNAME_ATTRIBUTE')
self.user_email_attribute = env('LDAP_USER_EMAIL_ATTRIBUTE')
self.user_photo_attribute = env('LDAP_USER_PHOTO_ATTRIBUTE', default=None)
self.user_attributes = [ "memberOf", "entryUUID", "initials", self.user_username_attribute, self.user_fullname_attribute, self.user_email_attribute ]
if self.user_photo_attribute:
self.user_attributes.append(self.user_photo_attribute)
self.con = ldap.initialize(self.url)
self.con.simple_bind_s(self.binddn, self.bindpassword)
def get_groups(self):
search_base = f"{self.group_base},{self.basedn}"
search_filter=f"(objectClass=groupOfNames)"
res = self.con.search(search_base, ldap.SCOPE_SUBTREE, search_filter, ['cn', 'description', 'o', 'entryUUID'])
result_set = {}
while True:
result_type, result_data = self.con.result(res, 0)
if (result_data == []):
break
else:
if result_type == ldap.RES_SEARCH_ENTRY:
ldap_data = {}
data = {}
for attribute in result_data[0][1]:
ldap_data[attribute] = [ val.decode() for val in result_data[0][1][attribute] ]
try:
data['dn'] = result_data[0][0]
data['name'] = ldap_data['cn'][0]
data['uuid'] = ldap_data['entryUUID'][0]
try:
data['description'] = ldap_data['description'][0]
except KeyError:
data['description'] = data['name']
result_set[data['name']] = data
except KeyError as e:
print(f"Skipping Ldap object {result_data[0][0]}, missing attribute {e}.")
return result_set
def get_group_name(self, dn):
res = self.con.search(dn, ldap.SCOPE_BASE, None, [self.group_name_attribute])
result_type, result_data = self.con.result(res, 0)
if result_type == ldap.RES_SEARCH_ENTRY:
return result_data[0][1][self.group_name_attribute][0].decode()
def get_users(self):
search_base = f"{self.user_base},{self.basedn}"
search_filter = ""
if self.user_group:
search_filter=f"(&(objectClass={self.user_objectclass})(memberof={self.user_group},{self.basedn}))"
else:
search_filter=f"(objectClass={self.user_objectclass})"
ldap_groups = self.get_groups()
res = self.con.search(search_base, ldap.SCOPE_SUBTREE, search_filter, self.user_attributes)
result_set = {}
while True:
result_type, result_data = self.con.result(res, 0)
if (result_data == []):
break
else:
if result_type == ldap.RES_SEARCH_ENTRY:
ldap_data = {}
data = {}
for attribute in result_data[0][1]:
if attribute == self.user_photo_attribute:
ldap_data[attribute] = result_data[0][1][attribute]
else:
ldap_data[attribute] = [ val.decode() for val in result_data[0][1][attribute] ]
try:
data['dn'] = result_data[0][0]
data['username'] = ldap_data[self.user_username_attribute][0]
data['full_name'] = ldap_data[self.user_fullname_attribute][0]
data['email'] = ldap_data[self.user_email_attribute][0]
data['uuid'] = ldap_data['entryUUID'][0]
try:
data['initials'] = ldap_data['initials'][0]
except KeyError:
data['initials'] = ''
try:
data['photo'] = ldap_data[self.user_photo_attribute][0]
data['photo_hash'] = hashlib.md5(data['photo']).digest()
except KeyError:
data['photo'] = None
data['is_superuser'] = f"{self.admin_group},{self.basedn}" in ldap_data['memberOf']
data['groups'] = []
for group in ldap_data['memberOf']:
if group.endswith(f"{self.group_base},{self.basedn}"):
data['groups'].append(ldap_groups[self.get_group_name(group)])
result_set[data['username']] = data
except KeyError as e:
print(f"Skipping Ldap object {result_data[0][0]}, missing attribute {e}.")
return result_set
def create_wekan_user(ldap_user):
user = { "_id": ldap_user['uuid'],
"username": ldap_user['username'],
"emails": [ { "address": ldap_user['email'], "verified": True } ],
"isAdmin": ldap_user['is_superuser'],
"loginDisabled": False,
"authenticationMethod": 'oauth2',
"sessionData": {},
"importUsernames": [ None ],
"teams": [],
"orgs": [],
"profile": {
"fullname": ldap_user['full_name'],
"avatarUrl": f"https://example.com/user/profile_picture/{ldap_user['username']}",
"initials": ldap_user['initials'],
"boardView": "board-view-swimlanes",
"listSortBy": "-modifiedAt",
},
"services": {
"oidc": {
"id": ldap_user['username'],
"username": ldap_user['username'],
"fullname": ldap_user['full_name'],
"email": ldap_user['email'],
"groups": [],
},
},
}
try:
mongodb_database["users"].insert_one(user)
print(f"Creating new Wekan user {ldap_user['username']}")
stats['created'] += 1
except DuplicateKeyError:
print(f"Wekan user {ldap_user['username']} already exists.")
update_wekan_user(ldap_user)
def update_wekan_user(ldap_user):
updated = False
user = mongodb_database["users"].find_one({"username": ldap_user['username']})
if user["emails"][0]["address"] != ldap_user['email']:
updated = True
user["emails"][0]["address"] = ldap_user['email']
if user["emails"][0]["verified"] != True:
updated = True
user["emails"][0]["verified"] = True
if user["isAdmin"] != ldap_user['is_superuser']:
updated = True
user["isAdmin"] = ldap_user['is_superuser']
try:
if user["loginDisabled"] != False:
updated = True
user["loginDisabled"] = False
except KeyError:
updated = True
user["loginDisabled"] = False
if user["profile"]["fullname"] != ldap_user['full_name']:
updated = True
user["profile"]["fullname"] = ldap_user['full_name']
if user["profile"]["avatarUrl"] != f"https://example.com/user/profile_picture/{ldap_user['username']}":
updated = True
user["profile"]["avatarUrl"] = f"https://example.com/user/profile_picture/{ldap_user['username']}"
if user["profile"]["initials"] != ldap_user['initials']:
updated = True
user["profile"]["initials"] = ldap_user['initials']
if user["services"]["oidc"]["fullname"] != ldap_user['full_name']:
updated = True
user["services"]["oidc"]["fullname"] = ldap_user['full_name']
if user["services"]["oidc"]["email"] != ldap_user['email']:
updated = True
user["services"]["oidc"]["email"] = ldap_user['email']
if updated:
print(f"Updated Wekan user {ldap_user['username']}")
stats['updated'] += 1
mongodb_database["users"].update_one({"username": ldap_user['username']}, {"$set": user})
def disable_wekan_user(username):
print(f"Disabling Wekan user {username}")
stats['disabled'] += 1
mongodb_database["users"].update_one({"username": username}, {"$set": {"loginDisabled": True}})
def create_wekan_team(ldap_group):
print(f"Creating new Wekan team {ldap_group['name']}")
stats['team_created'] += 1
team = { "_id": ldap_group['uuid'],
"teamShortName": ldap_group["name"],
"teamDisplayName": ldap_group["name"],
"teamDesc": ldap_group["description"],
"teamWebsite": "http://localhost",
"teamIsActive": True
}
mongodb_database["team"].insert_one(team)
def update_wekan_team(ldap_group):
updated = False
team = mongodb_database["team"].find_one({"_id": ldap_group['uuid']})
team_tmp = { "_id": ldap_group['uuid'],
"teamShortName": ldap_group["name"],
"teamDisplayName": ldap_group["name"],
"teamDesc": ldap_group["description"],
"teamWebsite": "http://localhost",
"teamIsActive": True
}
for key, value in team_tmp.items():
try:
if team[key] != value:
updated = True
break
except KeyError:
updated = True
if updated:
print(f"Updated Wekan team {ldap_group['name']}")
stats['team_updated'] += 1
mongodb_database["team"].update_one({"_id": ldap_group['uuid']}, {"$set": team_tmp})
def disable_wekan_team(teamname):
print(f"Disabling Wekan team {teamname}")
stats['team_disabled'] += 1
mongodb_database["team"].update_one({"teamShortName": teamname}, {"$set": {"teamIsActive": False}})
def update_wekan_team_memberships(ldap_user):
updated = False
user = mongodb_database["users"].find_one({"username": ldap_user['username']})
teams = user["teams"]
teams_tmp = []
for group in ldap_user["groups"]:
teams_tmp.append({
'teamId': group['uuid'],
'teamDisplayName': group['name'],
})
for team in teams_tmp:
if team not in teams:
updated = True
break
if len(teams) != len(teams_tmp):
updated = True
if updated:
print(f"Updated Wekan team memberships for {ldap_user['username']}")
stats['team_membership_update'] += 1
mongodb_database["users"].update_one({"username": ldap_user['username']}, {"$set": { "teams" : teams_tmp }})
def update_wekan_board_memberships(ldap_users):
for board in mongodb_database["boards"].find():
try:
if board['type'] != 'board':
continue
except KeyError:
continue
if not "teams" in board.keys():
continue
members = []
if "members" in board.keys():
members = board["members"]
members_tmp = []
for team in board["teams"]:
for username, user in ldap_users.items():
for group in user["groups"]:
if group['uuid'] == team['teamId']:
user_tmp = {
'userId': user['uuid'],
'isAdmin': user['is_superuser'],
'isActive': True,
'isNoComments': False,
'isCommentOnly': False,
'isWorker': False
}
if user_tmp not in members_tmp:
members_tmp.append(user_tmp.copy())
board_users = []
for card in mongodb_database["cards"].find({"boardId": board['_id']}):
if card['userId'] not in board_users:
board_users.append(card['userId'])
inactive_board_users = board_users.copy()
for member in members_tmp:
if member['userId'] in board_users:
inactive_board_users.remove(member['userId'])
for inactive_board_user in inactive_board_users:
user_tmp = {
'userId': inactive_board_user,
'isAdmin': False,
'isActive': False,
'isNoComments': False,
'isCommentOnly': False,
'isWorker': False
}
if user_tmp not in members_tmp:
members_tmp.append(user_tmp.copy())
if members != members_tmp:
print(f"Updated Wekan board membership for {board['title']}")
stats['board_membership_update'] += 1
mongodb_database["boards"].update_one({"_id": board["_id"]}, {"$set": { "members" : members_tmp }})
def ldap_sync():
print("Fetching users from LDAP")
ldap = LdapConnection()
ldap_users = ldap.get_users()
ldap_username_list = ldap_users.keys()
print("Fetching users from Wekan")
wekan_username_list = []
for user in mongodb_database["users"].find():
if not user['loginDisabled']:
wekan_username_list.append(user['username'])
print("Sorting users")
not_in_ldap = []
not_in_wekan = []
in_wekan = []
for ldap_username in ldap_username_list:
if ldap_username in wekan_username_list:
in_wekan.append(ldap_username)
else:
not_in_wekan.append(ldap_username)
for wekan_username in wekan_username_list:
if wekan_username not in ldap_username_list:
not_in_ldap.append(wekan_username)
print("Fetching groups from LDAP")
ldap_groups = ldap.get_groups()
ldap_groupname_list = ldap_groups.keys()
print("Fetching teams from Wekan")
wekan_teamname_list = []
for team in mongodb_database["team"].find():
if team['teamIsActive']:
wekan_teamname_list.append(team['teamShortName'])
print("Sorting groups")
group_not_in_ldap = []
group_not_in_wekan = []
group_in_wekan = []
for ldap_groupname in ldap_groupname_list:
if ldap_groupname in wekan_teamname_list:
group_in_wekan.append(ldap_groupname)
else:
group_not_in_wekan.append(ldap_groupname)
for wekan_teamname in wekan_teamname_list:
if wekan_teamname not in ldap_groupname_list:
group_not_in_ldap.append(wekan_teamname)
print("Processing users")
for user in not_in_wekan:
create_wekan_user(ldap_users[user])
for user in in_wekan:
update_wekan_user(ldap_users[user])
for user in not_in_ldap:
disable_wekan_user(user)
print("Processing groups")
for group in group_not_in_wekan:
create_wekan_team(ldap_groups[group])
for group in group_in_wekan:
update_wekan_team(ldap_groups[group])
for team in group_not_in_ldap:
disable_wekan_team(team)
for username, user in ldap_users.items():
update_wekan_team_memberships(user)
print("Updating board memberships")
update_wekan_board_memberships(ldap_users)
print()
print(f"Total users considered: {len(ldap_username_list)}")
print(f"Total groups considered: {len(ldap_groups)}")
print(f"Users created {stats['created']}")
print(f"Users updated {stats['updated']}")
print(f"Users disabled {stats['disabled']}")
print(f"Teams created {stats['team_created']}")
print(f"Teams updated {stats['team_updated']}")
print(f"Teams disabled {stats['team_disabled']}")
print(f"Team memberships updated: {stats['team_membership_update']}")
print(f"Board memberships updated: {stats['board_membership_update']}")
if __name__ == "__main__":
ldap_sync()

View file

@ -0,0 +1,19 @@
# Wekan on OpenShift
OpenShift Template for Wekan backed by MongoDB
#### Create Template
```sh
oc create -f wekan.yml
```
#### Delete Instance Resources
Clean up all resources created. Note label filters assume single instance of template deployed in the current namespace.
```sh
oc delete all -l app=wekan
oc delete pods -l app=wekan
oc delete persistentvolumeclaim -l app=wekan
oc delete serviceaccount -l app=wekan
oc delete secret -l app=wekan
```

View file

@ -0,0 +1,354 @@
---
apiVersion: v1
kind: Template
labels:
template: wekan-mongodb-persistent-template
message: |-
The following service(s) have been created in your project: ${WEKAN_SERVICE_NAME}.
metadata:
annotations:
description: |-
This template provides a Wekan instance backed by a standalone MongoDB
server. The database is stored on persistent storage.
iconClass: pficon-trend-up
openshift.io/display-name: Wekan backed by MongoDB
openshift.io/documentation-url: https://wekan.github.io/
openshift.io/long-description: This template provides a Wekan platform
with a MongoDB database created. The database is stored on persistent storage. The
database name, username, and password are chosen via parameters when provisioning
this service.
tags: wekan,kanban,mongodb
name: wekan-mongodb-persistent
objects:
- apiVersion: v1
kind: ServiceAccount
metadata:
name: ${WEKAN_SERVICE_NAME}
labels:
app: wekan
service: ${WEKAN_SERVICE_NAME}
- apiVersion: v1
kind: Secret
metadata:
annotations:
template.openshift.io/expose-admin_password: "{.data['database-admin-password']}"
template.openshift.io/expose-database_name: "{.data['database-name']}"
template.openshift.io/expose-password: "{.data['database-password']}"
template.openshift.io/expose-username: "{.data['database-user']}"
name: "${DATABASE_SERVICE_NAME}"
labels:
app: wekan
service: ${WEKAN_SERVICE_NAME}
stringData:
database-admin-password: "${MONGODB_ADMIN_PASSWORD}"
database-name: "${MONGODB_DATABASE}"
database-password: "${MONGODB_PASSWORD}"
database-user: "${MONGODB_USER}"
- apiVersion: v1
kind: Service
metadata:
annotations:
template.openshift.io/expose-uri: http://{.spec.clusterIP}:{.spec.ports[?(.name=="wekan")].port}
name: "${WEKAN_SERVICE_NAME}"
labels:
app: wekan
service: ${WEKAN_SERVICE_NAME}
spec:
ports:
- name: wekan
nodePort: 0
port: 8080
protocol: TCP
targetPort: 8080
selector:
name: "${WEKAN_SERVICE_NAME}"
sessionAffinity: ClientIP
type: ClusterIP
- apiVersion: v1
kind: Service
metadata:
annotations:
template.openshift.io/expose-uri: mongodb://{.spec.clusterIP}:{.spec.ports[?(.name=="mongo")].port}
name: "${DATABASE_SERVICE_NAME}"
labels:
app: wekan
service: ${WEKAN_SERVICE_NAME}
spec:
ports:
- name: mongo
nodePort: 0
port: 27017
protocol: TCP
targetPort: 27017
selector:
name: "${DATABASE_SERVICE_NAME}"
sessionAffinity: None
type: ClusterIP
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "${DATABASE_SERVICE_NAME}"
labels:
app: wekan
service: ${WEKAN_SERVICE_NAME}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "${VOLUME_CAPACITY}"
- apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
labels:
app: wekan
name: ${WEKAN_SERVICE_NAME}
spec:
tags:
- from:
kind: DockerImage
name: ${WEKAN_IMAGE}
generation: 2
name: latest
referencePolicy:
type: Source
- apiVersion: v1
kind: DeploymentConfig
metadata:
name: ${WEKAN_SERVICE_NAME}
labels:
app: wekan
service: ${WEKAN_SERVICE_NAME}
spec:
replicas: 1
selector:
app: wekan
deploymentconfig: ${WEKAN_SERVICE_NAME}
strategy:
type: Recreate
template:
metadata:
labels:
app: wekan
service: ${WEKAN_SERVICE_NAME}
deploymentconfig: ${WEKAN_SERVICE_NAME}
template: wekan
name: ${WEKAN_SERVICE_NAME}
spec:
containers:
- name: ${WEKAN_SERVICE_NAME}
image: ${WEKAN_IMAGE}
imagePullPolicy: Always
env:
- name: MONGO_URL
value: mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${DATABASE_SERVICE_NAME}:27017/${MONGODB_DATABASE}
- name: ROOT_URL
value: https://${FQDN}/
- name: PORT
value: "8080"
ports:
- containerPort: 8080
name: ${WEKAN_SERVICE_NAME}
protocol: TCP
terminationMessagePath: /dev/termination-log
livenessProbe:
failureThreshold: 30
httpGet:
path: /
port: 8080
initialDelaySeconds: 240
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
timeoutSeconds: 3
dnsPolicy: ClusterFirst
restartPolicy: Always
serviceAccount: ${WEKAN_SERVICE_NAME}
serviceAccountName: ${WEKAN_SERVICE_NAME}
terminationGracePeriodSeconds: 30
triggers:
- type: ConfigChange
- type: ImageChange
imageChangeParams:
automatic: true
containerNames:
- ${WEKAN_SERVICE_NAME}
from:
kind: ImageStreamTag
name: ${WEKAN_SERVICE_NAME}:latest
lastTriggeredImage: ""
- apiVersion: v1
kind: DeploymentConfig
metadata:
annotations:
template.alpha.openshift.io/wait-for-ready: 'true'
name: "${DATABASE_SERVICE_NAME}"
labels:
app: wekan
service: ${WEKAN_SERVICE_NAME}
spec:
replicas: 1
selector:
name: "${DATABASE_SERVICE_NAME}"
strategy:
type: Recreate
template:
metadata:
labels:
name: "${DATABASE_SERVICE_NAME}"
spec:
containers:
- capabilities: {}
env:
- name: MONGODB_USER
valueFrom:
secretKeyRef:
key: database-user
name: "${DATABASE_SERVICE_NAME}"
- name: MONGODB_PASSWORD
valueFrom:
secretKeyRef:
key: database-password
name: "${DATABASE_SERVICE_NAME}"
- name: MONGODB_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
key: database-admin-password
name: "${DATABASE_SERVICE_NAME}"
- name: MONGODB_DATABASE
valueFrom:
secretKeyRef:
key: database-name
name: "${DATABASE_SERVICE_NAME}"
image: " "
imagePullPolicy: IfNotPresent
livenessProbe:
initialDelaySeconds: 30
tcpSocket:
port: 27017
timeoutSeconds: 1
name: mongodb
ports:
- containerPort: 27017
protocol: TCP
readinessProbe:
exec:
command:
- "/bin/sh"
- "-i"
- "-c"
- mongo 127.0.0.1:27017/$MONGODB_DATABASE -u $MONGODB_USER -p $MONGODB_PASSWORD
--eval="quit()"
initialDelaySeconds: 3
timeoutSeconds: 1
resources:
limits:
memory: "${MEMORY_LIMIT}"
securityContext:
capabilities: {}
privileged: false
terminationMessagePath: "/dev/termination-log"
volumeMounts:
- mountPath: "/var/lib/mongodb/data"
name: "${DATABASE_SERVICE_NAME}-data"
dnsPolicy: ClusterFirst
restartPolicy: Always
volumes:
- name: "${DATABASE_SERVICE_NAME}-data"
persistentVolumeClaim:
claimName: "${DATABASE_SERVICE_NAME}"
triggers:
- imageChangeParams:
automatic: true
containerNames:
- mongodb
from:
kind: ImageStreamTag
name: mongodb:${MONGODB_VERSION}
namespace: "openshift"
lastTriggeredImage: ''
type: ImageChange
- type: ConfigChange
- apiVersion: route.openshift.io/v1
kind: Route
metadata:
labels:
app: wekan
service: wekan
template: wekan-mongodb-persistent-template
name: wekan
namespace: ${NAMESPACE}
spec:
host: ${FQDN}
port:
targetPort: wekan
tls:
termination: edge
to:
kind: Service
name: wekan
weight: 100
wildcardPolicy: None
parameters:
- description: The Fully Qualified Hostname (FQDN) of the application
displayName: FQDN
name: FQDN
required: true
- description: Maximum amount of memory the container can use.
displayName: Memory Limit
name: MEMORY_LIMIT
required: true
value: 512Mi
- description: The OpenShift Namespace where the ImageStream resides.
displayName: Namespace
name: NAMESPACE
- description: The name of the OpenShift Service exposed for the database.
displayName: Database Service Name
name: DATABASE_SERVICE_NAME
required: true
value: wekan-mongodb
- description: Username for MongoDB user that will be used for accessing the database.
displayName: MongoDB Connection Username
from: user[A-Z0-9]{3}
generate: expression
name: MONGODB_USER
required: true
- description: Password for the MongoDB connection user.
displayName: MongoDB Connection Password
from: "[a-zA-Z0-9]{16}"
generate: expression
name: MONGODB_PASSWORD
required: true
- description: Name of the MongoDB database accessed.
displayName: MongoDB Database Name
name: MONGODB_DATABASE
required: true
value: wekan
- description: Password for the database admin user.
displayName: MongoDB Admin Password
from: "[a-zA-Z0-9]{16}"
generate: expression
name: MONGODB_ADMIN_PASSWORD
required: true
- description: Volume space available for data, e.g. 512Mi, 2Gi.
displayName: Volume Capacity
name: VOLUME_CAPACITY
required: true
value: 1Gi
- description: Version of MongoDB image to be used (3.6, 4.0.10).
displayName: Version of MongoDB Image
name: MONGODB_VERSION
required: true
value: '3.6'
- name: WEKAN_SERVICE_NAME
displayName: Wekan Service Name
value: wekan
required: true
- name: WEKAN_IMAGE
displayName: Wekan Docker Image
value: quay.io/wekan/wekan
description: The metabase docker image to use
required: true

View file

@ -0,0 +1,21 @@
#!/bin/bash
set -euxo pipefail
# This file will store the config env variables needed by the app
readonly CONF=/build/env.config
# EMAIL_URL can also be set here by injecting another env variable in the template
# ROOT_URL can also be set at runtime, by querying AWS api or by using ingress annotations in the template for k8s.
cat >"${CONF}" <<'EOF'
export MONGO_URL=mongodb://{{DATABASE_USER}}:{{DATABASE_PASSWORD}}@{{DATABASE_HOST}}:{{DATABASE_PORT}}/{{DATABASE_NAME}}
export ROOT_URL=http://localhost
export PORT=3000
EOF
sed -i -e "s/{{DATABASE_USER}}/${DATABASE_USER}/" "${CONF}"
sed -i -e "s/{{DATABASE_PASSWORD}}/${DATABASE_PASSWORD}/" "${CONF}"
sed -i -e "s/{{DATABASE_HOST}}/${DATABASE_HOST}/" "${CONF}"
sed -i -e "s/{{DATABASE_PORT}}/${DATABASE_PORT}/" "${CONF}"
sed -i -e "s/{{DATABASE_NAME}}/${DATABASE_NAME}/" "${CONF}"

View file

@ -0,0 +1,90 @@
#!/bin/bash
set -euxo pipefail
BUILD_DEPS="bsdtar gnupg wget curl bzip2 python git ca-certificates perl-Digest-SHA"
NODE_VERSION=v14.21.3
#METEOR_RELEASE=1.6.0.1 - for Stacksmith, meteor-1.8 branch that could have METEOR@1.8.1-beta.8 or newer
USE_EDGE=false
METEOR_EDGE=1.5-beta.17
NPM_VERSION=latest
FIBERS_VERSION=4.0.1
ARCHITECTURE=linux-x64
sudo yum groupinstall -y 'Development Tools'
sudo yum install -y http://opensource.wandisco.com/centos/7/git/x86_64/wandisco-git-release-7-2.noarch.rpm
sudo yum install -y git
sudo useradd --user-group --system --home-dir /home/wekan wekan
sudo mkdir -p /home/wekan
sudo chown wekan:wekan /home/wekan/
# master branch that has newer Meteor that is compatible with MongoDB 4.x
sudo -u wekan git clone -b master https://github.com/wekan/wekan.git /home/wekan/app
sudo yum install -y ${BUILD_DEPS}
# Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
sudo cp $(which tar) $(which tar)~
sudo ln -sf $(which bsdtar) $(which tar)
# Install nodejs
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc
grep ${NODE_VERSION}-${ARCHITECTURE}.tar.gz SHASUMS256.txt | shasum -a 256 -c -
tar xvzf node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz
rm node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz
sudo mv node-${NODE_VERSION}-${ARCHITECTURE} /opt/nodejs
sudo rm -f /usr/bin/node
sudo rm -f /usr/bin/npm
sudo ln -s /opt/nodejs/bin/node /usr/bin/node || true
sudo ln -s /opt/nodejs/bin/npm /usr/bin/npm || true
sudo npm install -g npm@${NPM_VERSION}
sudo npm install -g node-gyp
sudo npm install -g --unsafe-perm fibers@${FIBERS_VERSION}
cd /home/wekan
#install meteor
sudo curl "https://install.meteor.com" -o /home/wekan/install_meteor.sh
sudo chmod +x /home/wekan/install_meteor.sh
sudo sed -i 's/VERBOSITY="--silent"/VERBOSITY="--progress-bar"/' ./install_meteor.sh
echo "Starting installation of "
sudo cat /home/wekan/app/.meteor/release
echo " ...\n"
# Check if opting for a release candidate instead of major release
if [ "$USE_EDGE" = false ]; then
sudo su -c '/home/wekan/install_meteor.sh' - wekan
else
sudo -u wekan git clone --recursive --depth 1 -b release/METEOR@${METEOR_EDGE} https://github.com/meteor/meteor.git /home/wekan/.meteor;
fi;
sudo sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js
sudo -u wekan /home/wekan/.meteor/meteor -- help
# Build app
cd /home/wekan/app
meteor=/home/wekan/.meteor/meteor
#sudo -u wekan ${meteor} add standard-minifier-js
sudo -u wekan ${meteor} npm install
sudo -u wekan ${meteor} build --directory /home/wekan/app_build
sudo rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
cd /home/wekan/app_build/bundle/programs/server/
sudo npm install
cd node_modules/fibers
node build.js
cd ../..
sudo chown -R wekan:wekan ./node_modules
#cd /home/wekan/app_build/bundle
#find . -name "*phantomjs*" | sudo xargs rm -rf
sudo mv /home/wekan/app_build/bundle /build

View file

@ -0,0 +1,28 @@
#!/bin/bash
set -euo pipefail
readonly CONF=/build/env.config
source ${CONF}
# wait for DB
check_db() {
mongo $MONGO_URL --eval "db.runCommand( { connectionStatus: 1} )" --quiet | python -c 'import json,sys;obj=json.load(sys.stdin);code = 0 if obj["ok"]==1 else 1; sys.exit(code);'
}
until check_db; do
period=5
echo "Cannot connect to db, waiting ${period} seconds before trying again..."
sleep ${period}
done
cd /build
echo "starting the wekan service..."
#---------------------------------------------------------------------
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
# Add more Node heap:
export NODE_OPTIONS="--max_old_space_size=4096"
# Add more stack:
#bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
bash -c "ulimit -s 65500; exec node main.js"
#---------------------------------------------------------------------
#node main.js

View file

@ -0,0 +1,72 @@
# v0.9 2018-12-22
* Update docker-compose.yml:
* Add more docs and environment settings
* Add latest Wekan and ToroDB.
Thanks to GitHub user xet7 for contributions.
# v0.8 2018-08-25
* Add OAuth2.
Thanks to GitHub users salleman33 and xet7 for their contributions.
# v0.7 2018-08-22
* Add browser-policy, trusted-url and webhooks-settings.
Thanks to GitHub users omarsy and xet7 for their contribution.
# v0.6 2018-08-03
* Update wekan-app container internal port to 8080.
Thanks to GitHub user xet7 for contributions.
# v0.5 2018-08-01
* Enable Wekan API by default, so that Export Board works.
* Add Matomo options.
Thanks to GitHub user xet7 for contributions.
# v0.4 2017-08-18
This release fixes following bugs:
* [ToroDB exits because of compound indexes not supported](https://github.com/torodb/stampede/issues/202).
Thanks to GitHub user teoincontatto for contributions.
# v0.3 2017-05-18
This release adds following new features:
* Use latest tag of Docker image.
Thanks to GitHub user xet7 for contributions.
# v0.2 2017-04-06
This release adds following new features:
* Use Meteor 1.4 based Docker image.
Thanks to GitHub users brylie and stephenmoloney for
their contributions.
MongoDB is kept at 3.2 because ToroDB is compatible
with it.
# v0.1 2017-02-13
This release adds following new features:
* Wekan <=> MongoDB <=> ToroDB => PostgreSQL read-only
mirroring for SQL access with any programming language
or Office package that has PostgreSQL support, like
newest LibreOffice 3.5.
Thanks to GitHub users mquandalle, stephenmoloney and xet7
for their contributions.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-2019 The Wekan team
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,64 @@
# Try FerretDB instead
https://github.com/wekan/wekan/wiki/PostgreSQL
ToroDB is not developed anymore.
ToroDB is compatible with MongoDB 3.0. WeKan 7.x is compatible with MongoDB 6.x.
# Docker: Wekan to PostgreSQL read-only mirroring
* [Wekan kanban board, made with Meteor.js framework, running on
Node.js](https://wekan.github.io) -- [GitHub](https://github.com/wekan/wekan)
* [MongoDB NoSQL database](https://www.mongodb.com)
* [ToroDB: MongoDB to PostgreSQL read-only mirroring, programmed with Java](https://www.8kdata.com/products) --
[GitHub](https://github.com/torodb/stampede) --
[Interview at FLOSS Weekly](https://twit.tv/shows/floss-weekly/episodes/377)
* [LibreOffice with native PostgreSQL support](https://www.libreoffice.org)
## Screenshot
![Screenshot of PostgreSQL with LibreOffice][screenshot]
## Install
1) Install docker-compose.
2) Clone this repo.
```bash
git clone https://github.com/wekan/wekan
cd torodb-postgresql
```
3) IMPORTANT: In docker-compose.yml, to use Wekan on local network, change ROOT_URL=http://localhost to http://IPADRESS like http://192.168.10.100 or http://example.com
4) OPTIONAL: In docker-compose.yml, change PostgreSQL database name, username and password from wekan to something else.
5) Write:
```bash
docker-compose up -d
```
6) Wekan is at http://IPADDRESS or http://example.com (port 80)
7) PostgreSQL connection URL for LibreOffice is `dbname=wekan hostaddr=127.0.0.1 port=15432 user=wekan password=wekan`.
In some other apps URL could be postgresql://127.0.0.1:15432/wekan , and
Username: wekan, Password: wekan , or others if you changed those at docker-compose.yml.
Do not write to PostgreSQL, as it's readonly mirror. Write to MongoDB or make
changes in Wekan. If server port 15432 open, PostgreSQL can be accessed also
remotely at local network at http://IPADDRESS:15432/wekan
8) MongoDB is at 127.0.0.1:28017
9) Wekan and databases bind to address 0.0.0.0 so could be also available to other
computers in network. I have not tested this.
10) [Restore your MongoDB data](https://github.com/wekan/wekan/wiki/Export-Docker-Mongo-Data).
## Feedback
[GitHub issue 787](https://github.com/wekan/wekan/issues/787)
[screenshot]: https://wekan.github.io/ToroDB.png

View file

@ -0,0 +1,708 @@
version: '2'
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
#---------------------------------------------------------------------------------------------------------
# ==== CREATING USERS AND LOGGING IN TO WEKAN ====
# https://github.com/wekan/wekan/wiki/Adding-users
#---------------------------------------------------------------------------------------------------------
# ==== FORGOT PASSWORD ====
# https://github.com/wekan/wekan/wiki/Forgot-Password
#---------------------------------------------------------------------------------------------------------
# ==== Upgrading Wekan to new version =====
# NOTE: MongoDB has changed from 3.x to 4.x, in that case you need backup/restore with --noIndexRestore
# see https://github.com/wekan/wekan/wiki/Backup
# 1) Stop Wekan:
# docker-compose stop
# 2) Remove old Wekan app (wekan-app only, not that wekan-db container that has all your data)
# docker rm wekan-app
# 3) Get newest docker-compose.yml from https://github.com/wekan/wekan to have correct image,
# for example: "image: quay.io/wekan/wekan" or version tag "image: quay.io/wekan/wekan:v4.52"
# 4) Start Wekan:
# docker-compose up -d
#----------------------------------------------------------------------------------
# ==== OPTIONAL: DEDICATED DOCKER USER ====
# 1) Optionally create a dedicated user for Wekan, for example:
# sudo useradd -d /home/wekan -m -s /bin/bash wekan
# 2) Add this user to the docker group, then logout+login or reboot:
# sudo usermod -aG docker wekan
# 3) Then login as user wekan.
# 4) Create this file /home/wekan/docker-compose.yml with your modifications.
#----------------------------------------------------------------------------------
# ==== RUN DOCKER AS SERVICE ====
# 1a) Running Docker as service, on Systemd like Debian 9, Ubuntu 16.04, CentOS 7:
# sudo systemctl enable docker
# sudo systemctl start docker
# 1b) Running Docker as service, on init.d like Debian 8, Ubuntu 14.04, CentOS 6:
# sudo update-rc.d docker defaults
# sudo service docker start
# ----------------------------------------------------------------------------------
# ==== USAGE OF THIS docker-compose.yml ====
# 1) For seeing does Wekan work, try this and check with your webbroser:
# docker-compose up
# 2) Stop Wekan and start Wekan in background:
# docker-compose stop
# docker-compose up -d
# 3) See running Docker containers:
# docker ps
# 4) Stop Docker containers:
# docker-compose stop
# ----------------------------------------------------------------------------------
# ===== INSIDE DOCKER CONTAINERS, AND BACKUP/RESTORE ====
# https://github.com/wekan/wekan/wiki/Backup
# If really necessary, repair MongoDB: https://github.com/wekan/wekan-mongodb/issues/6#issuecomment-424004116
# 1) Going inside containers:
# a) Wekan app, does not contain data
# docker exec -it wekan-app bash
# b) MongoDB, contains all data
# docker exec -it wekan-db bash
# 2) Copying database to outside of container:
# docker exec -it wekan-db bash
# cd /data
# mongodump
# exit
# docker cp wekan-db:/data/dump .
# 3) Restoring database
# # 1) Stop wekan
# docker stop wekan-app
# # 2) Go inside database container
# docker exec -it wekan-db bash
# # 3) and data directory
# cd /data
# # 4) Remove previos dump
# rm -rf dump
# # 5) Exit db container
# exit
# # 6) Copy dump to inside docker container
# docker cp dump wekan-db:/data/
# # 7) Go inside database container
# docker exec -it wekan-db bash
# # 8) and data directory
# cd /data
# # 9) Restore
# mongorestore --drop
# # 10) Exit db container
# exit
# # 11) Start wekan
# docker start wekan-app
#-------------------------------------------------------------------------
# 2020-12-03:
# - base images copied from Docker Hub to Quay to avoid Docker Hub rate limits,
# from: torodb/stampede:1.0.0, postgres:9.6, mongo:3.2
#-------------------------------------------------------------------------
services:
torodb-stampede:
image: quay.io/wekan/torodb-stampede:1.0.0
networks:
- wekan-tier
links:
- postgres
- mongodb
environment:
- POSTGRES_PASSWORD=wekan
- TORODB_SETUP=true
- TORODB_SYNC_SOURCE=mongodb:27017
- TORODB_BACKEND_HOST=postgres
- TORODB_BACKEND_PORT=5432
- TORODB_BACKEND_DATABASE=wekan
- TORODB_BACKEND_USER=wekan
- TORODB_BACKEND_PASSWORD=wekan
- DEBUG
volumes:
- /etc/localtime:/etc/localtime:ro
postgres:
image: quay.io/wekan/postgres:9.6
networks:
- wekan-tier
environment:
- POSTGRES_PASSWORD=wekan
ports:
- "5432:5432"
volumes:
- /etc/localtime:/etc/localtime:ro
mongodb:
image: mongo:3.2
networks:
- wekan-tier
ports:
- "27017:27017"
entrypoint:
- /bin/bash
- "-c"
- mongo --nodb --eval '
var db;
while (!db) {
try {
db = new Mongo("mongodb:27017").getDB("local");
} catch(ex) {}
sleep(3000);
};
rs.initiate({_id:"rs1",members:[{_id:0,host:"mongodb:27017"}]});
' 1>/dev/null 2>&1 &
mongod --replSet rs1
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- mongodb:/data/db
- mongodb-dump:/dump
wekan:
image: quay.io/wekan/wekan
container_name: wekan-app
restart: always
networks:
- wekan-tier
#-------------------------------------------------------------------------------------
# ==== BUILD wekan-app DOCKER CONTAINER FROM SOURCE, if you uncomment these ====
# ==== and use commands: docker-compose up -d --build
# ==== Dockerfile and source is at parent directory ..
#build:
# context: ..
# dockerfile: Dockerfile
#-------------------------------------------------------------------------------------
ports:
# Docker outsideport:insideport. Do not add anything extra here.
# For example, if you want to have wekan on port 3001,
# use 3001:8080 . Do not add any extra address etc here, that way it does not work.
- 80:8080
environment:
#-----------------------------------------------------------------
# ==== WRITEABLE PATH FOR FILE UPLOADS ====
- WRITABLE_PATH=/data
#-----------------------------------------------------------------
# ==== AWS S3 FOR FILES ====
# Any region. For example:
# us-standard,us-west-1,us-west-2,
# eu-west-1,eu-central-1,
# ap-southeast-1,ap-northeast-1,sa-east-1
#
#- S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx"}}'
#-----------------------------------------------------------------
# ==== MONGO_URL ====
- MONGO_URL=mongodb://mongodb:27017/wekan
#---------------------------------------------------------------
# ==== ROOT_URL SETTING ====
# Change ROOT_URL to your real Wekan URL, for example:
# If you have Caddy/Nginx/Apache providing SSL
# - https://example.com
# - https://boards.example.com
# This can be problematic with avatars https://github.com/wekan/wekan/issues/1776
# - https://example.com/wekan
# If without https, can be only wekan node, no need for Caddy/Nginx/Apache if you don't need them
# - http://example.com
# - http://boards.example.com
# - http://192.168.1.100 <=== using at local LAN
- ROOT_URL=http://localhost # <=== using only at same laptop/desktop where Wekan is installed
# ==== EMAIL SETTINGS ====
# Email settings are only at MAIL_URL and MAIL_FROM.
# Admin Panel has test button, but it's not used for settings.
# see https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
# For SSL in email, change smtp:// to smtps://
# NOTE: Special characters need to be url-encoded in MAIL_URL.
# You can encode those characters for example at: https://www.urlencoder.org
- MAIL_URL=smtp://user:pass@mailserver.example.com:25/
- MAIL_FROM='Example Wekan Support <support@example.com>'
# Currently MAIL_SERVICE is not in use.
#- MAIL_SERVICE=Outlook365
#- MAIL_SERVICE_USER=firstname.lastname@hotmail.com
#- MAIL_SERVICE_PASSWORD=SecretPassword
#---------------------------------------------------------------
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
# Add more Node heap, this is done by default at Dockerfile:
# - NODE_OPTIONS="--max_old_space_size=4096"
# Add more stack, this is done at Dockerfile:
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
#---------------------------------------------------------------
# ==== OPTIONAL: MONGO OPLOG SETTINGS =====
# https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-378343587
# We've fixed our CPU usage problem today with an environment
# change around Wekan. I wasn't aware during implementation
# that if you're using more than 1 instance of Wekan
# (or any MeteorJS based tool) you're supposed to set
# MONGO_OPLOG_URL as an environment variable.
# Without setting it, Meteor will perform a poll-and-diff
# update of it's dataset. With it, Meteor will update from
# the OPLOG. See here
# https://blog.meteor.com/tuning-meteor-mongo-livedata-for-scalability-13fe9deb8908
# After setting
# MONGO_OPLOG_URL=mongodb://<username>:<password>@<mongoDbURL>/local?authSource=admin&replicaSet=rsWekan
# the CPU usage for all Wekan instances dropped to an average
# of less than 10% with only occasional spikes to high usage
# (I guess when someone is doing a lot of work)
# - MONGO_OPLOG_URL=mongodb://<username>:<password>@<mongoDbURL>/local?authSource=admin&replicaSet=rsWekan
#---------------------------------------------------------------
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
# https://github.com/smeijer/kadira
# https://blog.meteor.com/kadira-apm-is-now-open-source-490469ffc85f
# - export KADIRA_OPTIONS_ENDPOINT=http://127.0.0.1:11011
#---------------------------------------------------------------
# ==== OPTIONAL: LOGS AND STATS ====
# https://github.com/wekan/wekan/wiki/Logs
#
# Daily export of Wekan changes as JSON to Logstash and ElasticSearch / Kibana (ELK)
# https://github.com/wekan/wekan-logstash
#
# Statistics Python script for Wekan Dashboard
# https://github.com/wekan/wekan-stats
#
# Console, file, and zulip logger on database changes https://github.com/wekan/wekan/pull/1010
# with fix to replace console.log by winston logger https://github.com/wekan/wekan/pull/1033
# but there could be bug https://github.com/wekan/wekan/issues/1094
#
# There is Feature Request: Logging date and time of all activity with summary reports,
# and requesting reason for changing card to other column https://github.com/wekan/wekan/issues/1598
#---------------------------------------------------------------
# ==== NUMBER OF SEARCH RESULTS PER PAGE BY DEFAULT ====
#- RESULTS_PER_PAGE=20
#---------------------------------------------------------------
# ==== WEKAN API AND EXPORT BOARD ====
# Wekan Export Board works when WITH_API=true.
# https://github.com/wekan/wekan/wiki/REST-API
# https://github.com/wekan/wekan-gogs
# If you disable Wekan API with false, Export Board does not work.
- WITH_API=true
#---------------------------------------------------------------
# ==== AFTER OIDC LOGIN, ADD USERS AUTOMATICALLY TO THIS BOARD ID ====
# https://github.com/wekan/wekan/pull/5098
#- DEFAULT_BOARD_ID=abcd1234
#---------------------------------------------------------------
# ==== PASSWORD BRUTE FORCE PROTECTION ====
#https://atmospherejs.com/lucasantoniassi/accounts-lockout
#Defaults below. Uncomment to change. wekan/server/accounts-lockout.js
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15
#---------------------------------------------------------------
# ==== ACCOUNT OPTIONS ====
# https://docs.meteor.com/api/accounts-multi.html#AccountsCommon-config
# Defaults below. Uncomment to change. wekan/server/accounts-common.js
# - ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90
#---------------------------------------------------------------
# ==== RICH TEXT EDITOR IN CARD COMMENTS ====
# https://github.com/wekan/wekan/pull/2560
- RICHER_CARD_COMMENT_EDITOR=false
#---------------------------------------------------------------
# ==== CARD OPENED, SEND WEBHOOK MESSAGE ====
# https://github.com/wekan/wekan/issues/2518
- CARD_OPENED_WEBHOOK_ENABLED=false
#---------------------------------------------------------------
# ==== Allow configuration to validate uploaded attachments ====
#-ATTACHMENTS_UPLOAD_EXTERNAL_PROGRAM=/usr/local/bin/avscan {file}
#-ATTACHMENTS_UPLOAD_MIME_TYPES=image/*,text/*
#-ATTACHMENTS_UPLOAD_MAX_SIZE=5000000
#---------------------------------------------------------------
# ==== Allow configuration to validate uploaded avatars ====
#-AVATARS_UPLOAD_EXTERNAL_PROGRAM=/usr/local/bin/avscan {file}
#-AVATARS_UPLOAD_MIME_TYPES=image/*
#-AVATARS_UPLOAD_MAX_SIZE=500000
#---------------------------------------------------------------
# ==== Allow to shrink attached/pasted image ====
# https://github.com/wekan/wekan/pull/2544
#-MAX_IMAGE_PIXEL=1024
#-IMAGE_COMPRESS_RATIO=80
#---------------------------------------------------------------
# ==== NOTIFICATION TRAY AFTER READ DAYS BEFORE REMOVE =====
# Number of days after a notification is read before we remove it.
# Default: 2
#- NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE=2
#---------------------------------------------------------------
# ==== BIGEVENTS DUE ETC NOTIFICATIONS =====
# https://github.com/wekan/wekan/pull/2541
# Introduced a system env var BIGEVENTS_PATTERN default as "NONE",
# so any activityType matches the pattern, system will send out
# notifications to all board members no matter they are watching
# or tracking the board or not. Owner of the wekan server can
# disable the feature by setting this variable to "NONE" or
# change the pattern to any valid regex. i.e. '|' delimited
# activityType names.
# a) Example
#- BIGEVENTS_PATTERN=due
# b) All
#- BIGEVENTS_PATTERN=received|start|due|end
# c) Disabled
- BIGEVENTS_PATTERN=NONE
#---------------------------------------------------------------
# ==== EMAIL DUE DATE NOTIFICATION =====
# https://github.com/wekan/wekan/pull/2536
# System timelines will be showing any user modification for
# dueat startat endat receivedat, also notification to
# the watchers and if any card is due, about due or past due.
#
# Notify due days, default 2 days before and after. 0 = due notifications disabled. Default: 2
#- NOTIFY_DUE_DAYS_BEFORE_AND_AFTER=2
#
# Notify due at hour of day. Default every morning at 8am. Can be 0-23.
# If env variable has parsing error, use default. Notification sent to watchers.
#- NOTIFY_DUE_AT_HOUR_OF_DAY=8
#-----------------------------------------------------------------
# ==== EMAIL NOTIFICATION TIMEOUT, ms =====
# Defaut: 30000 ms = 30s
#- EMAIL_NOTIFICATION_TIMEOUT=30000
#-----------------------------------------------------------------
# ==== CORS =====
# CORS: Set Access-Control-Allow-Origin header. Example: *
#- CORS=*
#-----------------------------------------------------------------
# ==== MATOMO INTEGRATION ====
# Optional: Integration with Matomo https://matomo.org that is installed to your server
# The address of the server where Matomo is hosted.
# example: - MATOMO_ADDRESS=https://example.com/matomo
#- MATOMO_ADDRESS=
# The value of the site ID given in Matomo server for Wekan
# example: - MATOMO_SITE_ID=12345
#- MATOMO_SITE_ID=
# The option do not track which enables users to not be tracked by matomo
# example: - MATOMO_DO_NOT_TRACK=false
#- MATOMO_DO_NOT_TRACK=
# The option that allows matomo to retrieve the username:
# example: MATOMO_WITH_USERNAME=true
#- MATOMO_WITH_USERNAME=false
#-----------------------------------------------------------------
# ==== METRICS ALLOWED IP ADDRESSES ====
# https://github.com/wekan/wekan/wiki/Metrics
#- METRICS_ALLOWED_IP_ADDRESSES=192.168.0.100,192.168.0.200
#-----------------------------------------------------------------
# ==== BROWSER POLICY AND TRUSTED IFRAME URL ====
# Enable browser policy and allow one trusted URL that can have iframe that has Wekan embedded inside.
# Setting this to false is not recommended, it also disables all other browser policy protections
# and allows all iframing etc. See wekan/server/policy.js
- BROWSER_POLICY_ENABLED=true
# When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside.
#- TRUSTED_URL=
#-----------------------------------------------------------------
# ==== OUTGOING WEBHOOKS ====
# What to send to Outgoing Webhook, or leave out. Example, that includes all that are default: cardId,listId,oldListId,boardId,comment,user,card,commentId .
# example: WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,commentId
#- WEBHOOKS_ATTRIBUTES=
#-----------------------------------------------------------------
# ==== AUTOLOGIN WITH OIDC/OAUTH2 ====
# https://github.com/wekan/wekan/wiki/autologin
#- OIDC_REDIRECTION_ENABLED=true
#---------------------------------------------
# ==== OAUTH2 ORACLE on premise identity manager OIM ====
#- ORACLE_OIM_ENABLED=true
#-----------------------------------------------------------------
# ==== OAUTH2 ONLY WITH OIDC AND DOORKEEPER AS INDENTITY PROVIDER
# https://github.com/wekan/wekan/issues/1874
# https://github.com/wekan/wekan/wiki/OAuth2
# Enable the OAuth2 connection
# example: OAUTH2_ENABLED=true
#- OAUTH2_ENABLED=false
# Optional OAuth2 CA Cert, see https://github.com/wekan/wekan/issues/3299
#- OAUTH2_CA_CERT=ABCD1234
# Use OAuth2 ADFS additional changes. Also needs OAUTH2_ENABLED=true setting.
#- OAUTH2_ADFS_ENABLED=false
# Azure AD B2C. https://github.com/wekan/wekan/issues/5242
#- OAUTH2_B2C_ENABLED=false
# OAuth2 docs: https://github.com/wekan/wekan/wiki/OAuth2
# OAuth2 Client ID, for example from Rocket.Chat. Example: abcde12345
# example: OAUTH2_CLIENT_ID=abcde12345
#- OAUTH2_CLIENT_ID=
# OAuth2 Secret, for example from Rocket.Chat: Example: 54321abcde
# example: OAUTH2_SECRET=54321abcde
#- OAUTH2_SECRET=
# OAuth2 Server URL, for example Rocket.Chat. Example: https://chat.example.com
# example: OAUTH2_SERVER_URL=https://chat.example.com
#- OAUTH2_SERVER_URL=
# OAuth2 Authorization Endpoint. Example: /oauth/authorize
# example: OAUTH2_AUTH_ENDPOINT=/oauth/authorize
#- OAUTH2_AUTH_ENDPOINT=
# OAuth2 Userinfo Endpoint. Example: /oauth/userinfo
# example: OAUTH2_USERINFO_ENDPOINT=/oauth/userinfo
#- OAUTH2_USERINFO_ENDPOINT=
# OAuth2 Token Endpoint. Example: /oauth/token
# example: OAUTH2_TOKEN_ENDPOINT=/oauth/token
#- OAUTH2_TOKEN_ENDPOINT=
#-----------------------------------------------------------------
# ==== LDAP ====
# https://github.com/wekan/wekan/wiki/LDAP
# For Snap settings see https://github.com/wekan/wekan-snap/wiki/Supported-settings-keys
# Most settings work both on Snap and Docker below.
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
#
# DEFAULT_AUTHENTICATION_METHOD : The default authentication method used if a user does not exist to create and authenticate. Can be set as ldap.
# example : DEFAULT_AUTHENTICATION_METHOD=ldap
#- DEFAULT_AUTHENTICATION_METHOD=
#
# LDAP_ENABLE : Enable or not the connection by the LDAP
# example : LDAP_ENABLE=true
#- LDAP_ENABLE=false
#
# LDAP_PORT : The port of the LDAP server
# example : LDAP_PORT=389
#- LDAP_PORT=389
#
# LDAP_HOST : The host server for the LDAP server
# example : LDAP_HOST=localhost
#- LDAP_HOST=
#
#-----------------------------------------------------------------
# ==== LDAP AD Simple Auth ====
#
# Set to true, if you want to connect with Active Directory by Simple Authentication.
# When using AD Simple Auth, LDAP_BASEDN is not needed.
#- LDAP_AD_SIMPLE_AUTH=true
#
# === Related settings ELSEWHERE IN THIS FILE, NOT HERE ===
#
# Option to login to the LDAP server with the user's own username and password, instead of
# an administrator key. Default: false (use administrator key). When using AD Simple Auth, set to true.
# Set to true, if the login user is used for binding. Used with AD Simple Auth.
# When using AD Simple Auth, LDAP_BASEDN is not needed.
##ELSEWHERE IN THIS SETTINGS FILE, NOT HERE: #- LDAP_USER_AUTHENTICATION=true
#
# Which field is used to find the user for the user authentication. Default: uid.
##ELSEWHERE IN THIS SETTINGS FILE, NOT HERE:#- LDAP_USER_AUTHENTICATION_FIELD=uid
#
# === LDAP Default Domain: 2 different use cases, a/b ===
#
# a) The default domain of the ldap it is used to create email if the field is not map
# correctly with the LDAP_SYNC_USER_DATA_FIELDMAP
#
# b) In case AD SimpleAuth is configured, the default domain is appended to the given
# loginname for creating the correct username for the bind request to AD.
#
# Example :
##ELSEWHERE IN THIS SETTINGS FILE, NOT HERE:- LDAP_DEFAULT_DOMAIN=mydomain.com
#
#-----------------------------------------------------------------
# ==== LDAP BASEDN Auth ====
#
# LDAP_BASEDN : The base DN for the LDAP Tree
# example : LDAP_BASEDN=ou=user,dc=example,dc=org
#- LDAP_BASEDN=
#
#-----------------------------------------------------------------
# LDAP_LOGIN_FALLBACK : Fallback on the default authentication method
# example : LDAP_LOGIN_FALLBACK=true
#- LDAP_LOGIN_FALLBACK=false
#
# LDAP_RECONNECT : Reconnect to the server if the connection is lost
# example : LDAP_RECONNECT=false
#- LDAP_RECONNECT=true
#
# LDAP_TIMEOUT : Overall timeout, in milliseconds
# example : LDAP_TIMEOUT=12345
#- LDAP_TIMEOUT=10000
#
# LDAP_IDLE_TIMEOUT : Specifies the timeout for idle LDAP connections in milliseconds
# example : LDAP_IDLE_TIMEOUT=12345
#- LDAP_IDLE_TIMEOUT=10000
#
# LDAP_CONNECT_TIMEOUT : Connection timeout, in milliseconds
# example : LDAP_CONNECT_TIMEOUT=12345
#- LDAP_CONNECT_TIMEOUT=10000
#
# LDAP_AUTHENTIFICATION : If the LDAP needs a user account to search
# example : LDAP_AUTHENTIFICATION=true
#- LDAP_AUTHENTIFICATION=false
#
# LDAP_AUTHENTIFICATION_USERDN : The search user DN
# example : LDAP_AUTHENTIFICATION_USERDN=cn=admin,dc=example,dc=org
#- LDAP_AUTHENTIFICATION_USERDN=
#
# LDAP_AUTHENTIFICATION_PASSWORD : The password for the search user
# example : AUTHENTIFICATION_PASSWORD=admin
#- LDAP_AUTHENTIFICATION_PASSWORD=
#
# LDAP_LOG_ENABLED : Enable logs for the module
# example : LDAP_LOG_ENABLED=true
#- LDAP_LOG_ENABLED=false
#
# LDAP_BACKGROUND_SYNC : If the sync of the users should be done in the background
# example : LDAP_BACKGROUND_SYNC=true
#- LDAP_BACKGROUND_SYNC=false
#
# At which interval does the background task sync.
# The format must be as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL='every 1 hour'
#
# LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED :
# example : LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=true
#- LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false
#
# LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS :
# example : LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=true
#- LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false
#
# LDAP_ENCRYPTION : If using LDAPS
# example : LDAP_ENCRYPTION=ssl
#- LDAP_ENCRYPTION=false
#
# LDAP_CA_CERT : The certification for the LDAPS server. Certificate needs to be included in this docker-compose.yml file.
# example : LDAP_CA_CERT=-----BEGIN CERTIFICATE-----MIIE+zCCA+OgAwIBAgIkAhwR/6TVLmdRY6hHxvUFWc0+Enmu/Hu6cj+G2FIdAgIC...-----END CERTIFICATE-----
#- LDAP_CA_CERT=
#
# LDAP_REJECT_UNAUTHORIZED : Reject Unauthorized Certificate
# example : LDAP_REJECT_UNAUTHORIZED=true
#- LDAP_REJECT_UNAUTHORIZED=false
#
# LDAP_USER_SEARCH_FILTER : Optional extra LDAP filters. Don't forget the outmost enclosing parentheses if needed
# example : LDAP_USER_SEARCH_FILTER=
#- LDAP_USER_SEARCH_FILTER=
#
# LDAP_USER_SEARCH_SCOPE : base (search only in the provided DN), one (search only in the provided DN and one level deep), or sub (search the whole subtree)
# example : LDAP_USER_SEARCH_SCOPE=one
#- LDAP_USER_SEARCH_SCOPE=
#
# LDAP_USER_SEARCH_FIELD : Which field is used to find the user
# example : LDAP_USER_SEARCH_FIELD=uid
#- LDAP_USER_SEARCH_FIELD=
#
# LDAP_SEARCH_PAGE_SIZE : Used for pagination (0=unlimited)
# example : LDAP_SEARCH_PAGE_SIZE=12345
#- LDAP_SEARCH_PAGE_SIZE=0
#
# LDAP_SEARCH_SIZE_LIMIT : The limit number of entries (0=unlimited)
# example : LDAP_SEARCH_SIZE_LIMIT=12345
#- LDAP_SEARCH_SIZE_LIMIT=0
#
# LDAP_GROUP_FILTER_ENABLE : Enable group filtering. Note the authenticated ldap user must be able to query all relevant group data with own login data from ldap
# example : LDAP_GROUP_FILTER_ENABLE=true
#- LDAP_GROUP_FILTER_ENABLE=false
#
# LDAP_GROUP_FILTER_OBJECTCLASS : The object class for filtering
# example : LDAP_GROUP_FILTER_OBJECTCLASS=group
#- LDAP_GROUP_FILTER_OBJECTCLASS=
#
# LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE : The attribute of a group identifying it
# example : LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=cn
#- LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=
#
# LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE : The attribute inside a group object listing its members
# example : member
#- LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE=
#
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT : The format of the value of LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE
# example : dn
#- LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT=
#
# LDAP_GROUP_FILTER_GROUP_NAME : The group name (id) that matches all users
# example : wekan_users
#- LDAP_GROUP_FILTER_GROUP_NAME=
#
# LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier)
# example : LDAP_UNIQUE_IDENTIFIER_FIELD=guid
#- LDAP_UNIQUE_IDENTIFIER_FIELD=
#
# LDAP_UTF8_NAMES_SLUGIFY : Convert the username to utf8
# example : LDAP_UTF8_NAMES_SLUGIFY=false
#- LDAP_UTF8_NAMES_SLUGIFY=true
#
# LDAP_USERNAME_FIELD : Which field contains the ldap username
# example : LDAP_USERNAME_FIELD=username
#- LDAP_USERNAME_FIELD=
#
# LDAP_FULLNAME_FIELD : Which field contains the ldap fullname
# example : LDAP_FULLNAME_FIELD=fullname
#- LDAP_FULLNAME_FIELD=
#
# LDAP_MERGE_EXISTING_USERS :
# example : LDAP_MERGE_EXISTING_USERS=true
#- LDAP_MERGE_EXISTING_USERS=false
#
# LDAP_SYNC_USER_DATA :
# example : LDAP_SYNC_USER_DATA=true
#- LDAP_SYNC_USER_DATA=false
#
# LDAP_SYNC_USER_DATA_FIELDMAP :
# example : LDAP_SYNC_USER_DATA_FIELDMAP={"cn":"name", "mail":"email"}
#- LDAP_SYNC_USER_DATA_FIELDMAP=
#
# LDAP_SYNC_GROUP_ROLES :
# example :
#- LDAP_SYNC_GROUP_ROLES=
#
# LDAP_DEFAULT_DOMAIN : The default domain of the ldap it is used to create email if the field is not map correctly with the LDAP_SYNC_USER_DATA_FIELDMAP
# example :
#- LDAP_DEFAULT_DOMAIN=
#
#---------------------------------------------------------------------
# ==== LOGOUT TIMER, probably does not work yet ====
# LOGOUT_WITH_TIMER : Enables or not the option logout with timer
# example : LOGOUT_WITH_TIMER=true
#- LOGOUT_WITH_TIMER=
#
# LOGOUT_IN : The number of days
# example : LOGOUT_IN=1
#- LOGOUT_IN=
#
# LOGOUT_ON_HOURS : The number of hours
# example : LOGOUT_ON_HOURS=9
#- LOGOUT_ON_HOURS=
#
# LOGOUT_ON_MINUTES : The number of minutes
# example : LOGOUT_ON_MINUTES=55
#- LOGOUT_ON_MINUTES=
#---------------------------------------------------------------------
# PASSWORD_LOGIN_ENABLED : Enable or not the password login form.
# example: PASSWORD_LOGIN_ENABLED=false
# - PASSWORD_LOGIN_ENABLED
#-------------------------------------------------------------------
#- CAS_ENABLED=true
#- CAS_BASE_URL=https://cas.example.com/cas
#- CAS_LOGIN_URL=https://cas.example.com/login
#- CAS_VALIDATE_URL=https://cas.example.com/cas/p3/serviceValidate
#---------------------------------------------------------------------
#- SAML_ENABLED=true
#- SAML_PROVIDER=
#- SAML_ENTRYPOINT=
#- SAML_ISSUER=
#- SAML_CERT=
#- SAML_IDPSLO_REDIRECTURL=
#- SAML_PRIVATE_KEYFILE=
#- SAML_PUBLIC_CERTFILE=
#- SAML_IDENTIFIER_FORMAT=
#- SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE=
#- SAML_ATTRIBUTES=
#---------------------------------------------------------------------
# Wait spinner to use
#- WAIT_SPINNER=Bounce
#---------------------------------------------------------------------
depends_on:
- mongodb
volumes:
- /etc/localtime:/etc/localtime:ro
- wekan-files:/data:rw
#---------------------------------------------------------------------------------
# ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====
# When using Wekan both at office LAN and remote VPN:
# 1) Have above Wekan docker container config with LAN IP address
# 2) Copy all of above wekan container config below, look above of this part above and all config below it,
# before above depends_on: part:
#
# wekan:
# #-------------------------------------------------------------------------------------
# # ==== MONGODB AND METEOR VERSION ====
# # a) For Wekan Meteor 1.8.x version at meteor-1.8 branch, .....
#
#
# and change name to different name like wekan2 or wekanvpn, and change ROOT_URL to server VPN IP
# address.
# 3) This way both Wekan containers can use same MongoDB database
# and see the same Wekan boards.
# 4) You could also add 3rd Wekan container for 3rd network etc.
# EXAMPLE:
# wekan2:
# ....COPY CONFIG FROM ABOVE TO HERE...
# environment:
# - ROOT_URL='http://10.10.10.10'
# ...COPY CONFIG FROM ABOVE TO HERE...
#---------------------------------------------------------------------------------
volumes:
wekan-files:
driver: local
mongodb:
driver: local
mongodb-dump:
driver: local
networks:
wekan-tier:
driver: bridge

View file

@ -0,0 +1,92 @@
user www-data;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
map $http_host $this_host {
"" $host;
default $http_host;
}
map $http_x_forwarded_proto $the_scheme {
default $http_x_forwarded_proto;
"" $scheme;
}
map $http_x_forwarded_host $the_host {
default $http_x_forwarded_host;
"" $this_host;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen 443 ssl;
if ($scheme = http) {
rewrite ^ https://$host$request_uri? permanent;
}
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+AESGCM:EECDH+CHACHA20:EECDH+AES;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_ecdh_curve sect571r1:secp521r1:brainpoolP512r1:secp384r1;
add_header Strict-Transport-Security "max-age=31536000; preload";
# Add headers to serve security related headers
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header X-Download-Options noopen;
add_header X-Permitted-Cross-Domain-Policies none;
add_header Referrer-Policy "same-origin";
root /var/www/html;
client_max_body_size 10G; # 0=unlimited - set max upload size
fastcgi_buffers 64 4K;
gzip off;
location / {
proxy_pass http://wekan:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
}

View file

@ -0,0 +1 @@
PLACE YOUR SSL Certificates in this folder