Compare commits

...

No commits in common. "main" and "feature/extend_role_editor_to_admin_tool" have entirely different histories.

93 changed files with 6044 additions and 2 deletions

436
.gitignore vendored Normal file
View File

@ -0,0 +1,436 @@
Skip to content
Pull requests
Issues
Codespaces
Marketplace
Explore
@danielschick
github /
gitignore
Public
Fork your own copy of github/gitignore
Code
Pull requests 371
Actions
Security
Insights
Beta Try the new code view
gitignore/VisualStudio.gitignore
@n0099
n0099 [VisualStudio.gitignore] remove a trailing space
Latest commit 491040e Jan 26, 2022
History
165 contributors
@shiftkey
@arcresu
@aroben
@bbodenmiller
@HassanHashemi
@haacked
@niik
@AArnott
@sayedihashimi
@saschanaz
@bdougie
@OsirisTerje
398 lines (319 sloc) 6.7 KB
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp

26
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "src/server/BreCal",
"FLASK_DEBUG": "1",
"SECRET_KEY" : "zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ" // https://randomkeygen.com/
},
"args": [
"run" //,
// "--no-debugger",
//"--no-reload"
],
"jinja": true,
"justMyCode": true
}
]
}

View File

@ -1,2 +1,23 @@
# brecal
Bremen calling
# Bremen Calling
___
Projekt zur verbesserten Kommunikation der maritimen Partner bei Schiffsanläufen in Bremen.
## Anforderungen
## Architektur
Die Architektur besteht aus einer Datenbank und einem in Python implementierten Backend, das eine API zu Verfügung stellt. Diese API ist als OpenAPI 3.0 spezifiziert.
Die Anwendung selbst kommuniziert nur über diese API mit der Datenbank. Es sind damit unterschiedliche Anwendungsplattformen denkbar, etwa eine Web-, Mobile- oder Windows Desktop Anwendung.
In dieser [Folie](docs/Architektur.pptx) ist ein Bild / Überblick enthalten.
Ein erster Gedanke des Datenbank-Layouts sieht folgendermaßen aus:
![image](docs/datenbank.jpeg)
## Entwicklung
### Postman
Zum Debuggen der Flask App verwende ich dieses Tutorial:
https://code.visualstudio.com/docs/python/tutorial-flask#_create-a-project-environment-for-the-flask-tutorial

Binary file not shown.

BIN
docs/AMPELKONZEPT_V1.docx Normal file

Binary file not shown.

BIN
docs/Ablaufplan.pptx Normal file

Binary file not shown.

BIN
docs/Arbeitspakete.xlsx Normal file

Binary file not shown.

BIN
docs/Architektur.pptx Normal file

Binary file not shown.

BIN
docs/BremenCalling.pptx Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/Gesamtansicht.pptx Normal file

Binary file not shown.

BIN
docs/UserStories.xlsx Normal file

Binary file not shown.

BIN
docs/datenbank.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,426 @@
{
"info": {
"_postman_id": "9242b2d1-196b-4b2e-af57-c0e9eb141dba",
"name": "BreCal",
"description": "Bremen Calling relevant API calls",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "10427908"
},
"item": [
{
"name": "Login user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.environment.set(\"LOGON_TOKEN\", responseBody)"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"username\" : \"Londo\",\r\n \"password\" : \"Hallowach\"\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{SCHEMA}}{{PATH}}/login",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"login"
]
}
},
"response": []
},
{
"name": "Participant GET",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{SCHEMA}}{{PATH}}/participant?user_id=1",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"participant"
],
"query": [
{
"key": "user_id",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "Shipcalls GET",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{SCHEMA}}{{PATH}}/shipcalls",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"shipcalls"
]
}
},
"response": []
},
{
"name": "Shipcalls POST",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"ship_id\" : 1,\r\n \"type\" : 1,\r\n \"eta\" : \"2023-07-23T07:18:19\",\r\n \"voyage\" : \"43B\",\r\n \"tug_required\" : false,\r\n \"pilot_required\" : true,\r\n \"flags\" : 0,\r\n \"pier_side\" : false,\r\n \"bunkering\" : true,\r\n \"recommended_tugs\" : 2\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{SCHEMA}}{{PATH}}/shipcalls",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"shipcalls"
]
}
},
"response": []
},
{
"name": "Shipcalls PUT",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"id\" : 2, \r\n \"recommended_tugs\" : 3\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{SCHEMA}}{{PATH}}/shipcalls",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"shipcalls"
]
}
},
"response": []
},
{
"name": "Berths GET",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{SCHEMA}}{{PATH}}/berths",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"berths"
]
}
},
"response": []
},
{
"name": "Notifications GET",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{SCHEMA}}{{PATH}}/notifications?participant_id=1",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"notifications"
],
"query": [
{
"key": "participant_id",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "Ships GET",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{SCHEMA}}{{PATH}}/ships",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"ships"
]
}
},
"response": []
},
{
"name": "Times GET",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{SCHEMA}}{{PATH}}/times?shipcall_id=1",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"times"
],
"query": [
{
"key": "shipcall_id",
"value": "1"
}
]
}
},
"response": []
},
{
"name": "Times POST",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"start_planned\" : \"2023-04-18T07:18:19\",\r\n \"end_planned\" : \"2023-04-18T09:18:19\", \r\n \"shipcall_id\" : 1,\r\n \"participant_id\" : 1\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{SCHEMA}}{{PATH}}/times",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"times"
]
}
},
"response": []
},
{
"name": "Times PUT",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"start_planned\" : \"2023-05-18T07:18:19\",\r\n \"end_planned\" : \"2023-05-18T09:18:19\", \r\n \"id\" : 1\r\n}"
},
"url": {
"raw": "{{SCHEMA}}{{PATH}}/times",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"times"
]
}
},
"response": []
},
{
"name": "Times DELETE",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "DELETE",
"header": [],
"url": {
"raw": "{{SCHEMA}}{{PATH}}/times?id=3",
"host": [
"{{SCHEMA}}{{PATH}}"
],
"path": [
"times"
],
"query": [
{
"key": "id",
"value": "3"
}
]
}
},
"response": []
}
],
"auth": {
"type": "bearer"
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
]
}

50
misc/Readme.md Normal file
View File

@ -0,0 +1,50 @@
# Database Bremen Calling
## Server
mariadb v 10
## Getting started
- Execute 'create_schema.sql' and import 'sample_data.sql'
If the database is updated or changed, please update these files.
To avoid errors, it is best to drop the entire database, edit the create script and re-import the sample data.
## Creating the dump file
```bash
mysqldump -u [username] -p --no-create-info --complete-insert bremen_calling > sample_data.sql
```
## Removing existing tables
We want only to remove the tables in order to preserve user privileges
```sql
SELECT concat('DROP TABLE IF EXISTS `', table_name, '`;')
FROM information_schema.tables
WHERE table_schema = 'bremen_calling';
```
outputs complete drop statements
```sql
SET FOREIGN_KEY_CHECKS = 0;
-- Your semicolon separated list of DROP statements here
DROP TABLE IF EXISTS `notification`;
DROP TABLE IF EXISTS `role`;
DROP TABLE IF EXISTS `ship`;
DROP TABLE IF EXISTS `participant`;
DROP TABLE IF EXISTS `times`;
DROP TABLE IF EXISTS `role_securable_map`;
DROP TABLE IF EXISTS `user_role_map`;
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS `securable`;
DROP TABLE IF EXISTS `shipcall_participant_map`;
DROP TABLE IF EXISTS `berth`;
DROP TABLE IF EXISTS `shipcall`;
SET FOREIGN_KEY_CHECKS = 1;
```

View File

@ -0,0 +1,27 @@
{
"id": "a3b2b291-6ec7-4af8-9ba6-57448547f71b",
"name": "Remote BreCal EMSWE",
"values": [
{
"key": "PATH",
"value": "brecal.bsmd-emswe.eu/",
"type": "default",
"enabled": true
},
{
"key": "LOGON_TOKEN",
"value": "",
"type": "any",
"enabled": true
},
{
"key": "SCHEMA",
"value": "https://",
"type": "default",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2023-06-27T09:33:59.460Z",
"_postman_exported_using": "Postman/10.15.4"
}

32
misc/add_user.py Normal file
View File

@ -0,0 +1,32 @@
import mysql.connector
import os
import json
import bcrypt
config_path = '../src/server/BreCal/connection_data.json'
print (os.getcwd())
if not os.path.exists(config_path):
print ('cannot find ' + config_path)
exit(1)
f = open(config_path);
connection_data = json.load(f)
mydb = mysql.connector.connect(host=connection_data["host"], user=connection_data["user"],
password = connection_data["password"], database=connection_data["database"])
print(mydb)
# insert a new user
participant_id = 1
first_name = "Londo"
last_name = "Mollari"
user_name = "Londo"
user_email = "l.mollari@centauri.gov"
user_phone = "+01 555 324 2313"
password = "Hallowach"
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt( 12 )).decode('utf8')
query = "INSERT INTO user (participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash) VALUES (" + str(participant_id) + ",\"" + first_name + "\",\"" + last_name + "\",\"" + user_name + "\",\"" + user_email + "\",\"" + user_phone + "\",\"" + password_hash + "\")"
with mydb.cursor() as cursor:
cursor.execute(query)
mydb.commit()

209
misc/create_schema.sql Normal file
View File

@ -0,0 +1,209 @@
CREATE DATABASE `bremen_calling` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `bremen_calling`
CREATE TABLE `participant` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`street` varchar(128) DEFAULT NULL,
`postal_code` varchar(5) DEFAULT NULL,
`city` varchar(64) DEFAULT NULL,
`flags` int(10) unsigned DEFAULT NULL,
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='An organization taking part';
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`participant_id` int(11) UNSIGNED DEFAULT NULL,
`first_name` varchar(45) DEFAULT NULL,
`last_name` varchar(45) DEFAULT NULL,
`user_name` varchar(45) DEFAULT NULL,
`user_email` varchar(128) DEFAULT NULL,
`user_phone` varchar(128) DEFAULT NULL,
`password_hash` varchar(128) DEFAULT NULL,
`api_key` varchar(256) DEFAULT NULL,
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_USER_PART` (`participant_id`),
CONSTRAINT `FK_USER_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='member of a participant';
CREATE TABLE `berth` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(128) NULL DEFAULT NULL COMMENT 'Descriptive name',
`participant_id` INT(10) UNSIGNED NULL DEFAULT NULL COMMENT 'If berth belongs to a participant, reference it here',
`lock` BIT(1) NULL DEFAULT NULL COMMENT 'The lock must be used',
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_BERTH_PART` (`participant_id`),
CONSTRAINT `FK_BERTH_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) COMMENT='Berth of ship for a ship call' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `ship` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
`imo` int(11) DEFAULT NULL,
`callsign` varchar(8) DEFAULT NULL,
`participant_id` INT(11) UNSIGNED NULL DEFAULT NULL,
`length` FLOAT NULL DEFAULT NULL,
`width` FLOAT NULL DEFAULT NULL,
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_SHIP_PARTICIPANT` (`participant_id`),
CONSTRAINT `FK_SHIP_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `shipcall` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`ship_id` INT(11) UNSIGNED NULL DEFAULT NULL,
`type` TINYINT(4) NULL DEFAULT NULL,
`eta` DATETIME NULL DEFAULT NULL,
`voyage` VARCHAR(16) NULL DEFAULT NULL,
`etd` DATETIME NULL DEFAULT NULL,
`arrival_berth_id` INT(10) UNSIGNED NULL DEFAULT NULL,
`departure_berth_id` INT(10) UNSIGNED NULL DEFAULT NULL,
`tug_required` BIT(1) NULL DEFAULT NULL,
`pilot_required` BIT(1) NULL DEFAULT NULL,
`flags` INT(10) UNSIGNED NULL DEFAULT 0,
`pier_side` BIT(1) NULL DEFAULT NULL,
`bunkering` BIT(1) NULL DEFAULT NULL,
`replenishing` BIT(1) NULL DEFAULT NULL,
`draft` FLOAT NULL DEFAULT NULL,
`tidal_window_from` DATETIME NULL DEFAULT NULL,
`tidal_window_to` DATETIME NULL DEFAULT NULL,
`rain_sensitive_cargo` BIT(1) NULL DEFAULT b'0',
`recommended_tugs` INT(11) NULL DEFAULT 0,
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_SHIPCALL_SHIP` (`ship_id`),
INDEX `FK_SHIPCALL_BERTH_ARRIVAL` (`arrival_berth_id`),
INDEX `FK_SHIPCALL_BERTH_DEPARTURE` (`departure_berth_id`),
CONSTRAINT `FK_SHIPCALL_BERTH_ARRIVAL` FOREIGN KEY (`arrival_berth_id`) REFERENCES `berth` (`id`),
CONSTRAINT `FK_SHIPCALL_BERTH_DEPARTURE` FOREIGN KEY (`departure_berth_id`) REFERENCES `berth` (`id`),
CONSTRAINT `FK_SHIPCALL_SHIP` FOREIGN KEY (`ship_id`) REFERENCES `ship` (`id`)
) COMMENT='Incoming, outgoing or moving to another berth' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `times` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`start_planned` datetime DEFAULT NULL,
`end_planned` datetime DEFAULT NULL,
`duration_planned` int(11) DEFAULT NULL,
`start_actual` datetime DEFAULT NULL,
`end_actual` datetime DEFAULT NULL,
`duration_actual` int(11) DEFAULT NULL,
`shipcall_id` int(11) UNSIGNED DEFAULT NULL,
`participant_id` int(11) UNSIGNED DEFAULT NULL,
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_TIME_SHIPCALL` (`shipcall_id`),
INDEX `FK_TIME_PART` (`participant_id`),
CONSTRAINT `FK_TIME_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_TIME_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='the planned time for the participants work';
CREATE TABLE `shipcall_participant_map` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`shipcall_id` int(10) unsigned DEFAULT NULL,
`participant_id` int(10) unsigned DEFAULT NULL,
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_MAP_PARTICIPANT_SHIPCALL` (`shipcall_id`),
INDEX `FK_MAP_SHIPCALL_PARTICIPANT` (`participant_id`),
CONSTRAINT `FK_MAP_PARTICIPANT_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
CONSTRAINT `FK_MAP_SHIPCALL_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Associates a participant with a shipcall';
CREATE TABLE `shipcall_tug_map` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`shipcall_id` INT(11) UNSIGNED NOT NULL COMMENT 'Ref to ship call',
`ship_id` INT(11) UNSIGNED NOT NULL COMMENT 'Ref to ship (that is a tug)',
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_SCT_SHIP` (`ship_id`),
INDEX `FK_SCT_SHIPCALL` (`shipcall_id`),
CONSTRAINT `FK_SCT_SHIP` FOREIGN KEY (`ship_id`) REFERENCES `ship` (`id`),
CONSTRAINT `FK_SCT_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`)
) COMMENT='Mapping table that assigns tugs to a ship call' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `notification` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`times_id` INT(11) UNSIGNED NOT NULL COMMENT 'times record that caused the notification',
`participant_id` INT(11) UNSIGNED NOT NULL COMMENT 'participant ref',
`acknowledged` BIT(1) NULL DEFAULT b'0' COMMENT 'true if UI acknowledged',
`level` TINYINT(4) NULL DEFAULT NULL COMMENT 'severity of the notification',
`type` TINYINT(4) NULL DEFAULT NULL COMMENT 'Email/UI/Other',
`message` VARCHAR(256) NULL DEFAULT NULL COMMENT 'individual message',
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_NOT_TIMES` (`times_id`),
INDEX `FK_NOT_PART` (`participant_id`),
CONSTRAINT `FK_NOT_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_NOT_TIMES` FOREIGN KEY (`times_id`) REFERENCES `times` (`id`)
) COMMENT='An entry corresponds to an alarm given by a violated rule during times update' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `role` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL DEFAULT '0' COMMENT 'unique role name',
`description` VARCHAR(255) NULL DEFAULT '0' COMMENT 'role description',
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE INDEX `name` (`name`)
) COMMENT='logical group of securables for one or more user' DEFAULT CHARSET=utf8mb4 ENGINE=InnoDB;
CREATE TABLE `securable` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL DEFAULT '',
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE INDEX `name` (`name`)
) COMMENT='Actual permission on a single(!) feature or operation' DEFAULT CHARSET=utf8mb4 ENGINE=InnoDB;
CREATE TABLE `user_role_map` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`role_id` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_USER_ROLE` (`user_id`),
INDEX `FK_ROLE_USER` (`role_id`),
CONSTRAINT `FK_ROLE_USER` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
CONSTRAINT `FK_USER_ROLE` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) COMMENT='Assigns a user to a role' DEFAULT CHARSET=utf8mb4 ENGINE=InnoDB;
CREATE TABLE `role_securable_map` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`role_id` INT(10) UNSIGNED NOT NULL,
`securable_id` INT(10) UNSIGNED NOT NULL,
`created` DATETIME NULL DEFAULT current_timestamp(),
`modified` DATETIME NULL DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
INDEX `FK_ROLE_SECURABLE` (`role_id`),
INDEX `FK_SECURABLE_ROLE` (`securable_id`),
CONSTRAINT `FK_ROLE_SECURABLE` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
CONSTRAINT `FK_SECURABLE_ROLE` FOREIGN KEY (`securable_id`) REFERENCES `securable` (`id`)
) COMMENT='Assigns securables to roles' DEFAULT CHARSET=utf8mb4 ENGINE=InnoDB;

515
misc/index.yaml Normal file
View File

@ -0,0 +1,515 @@
openapi: '3.0.0'
info:
version: '1.0.0'
title: 'Bremen calling API'
description: Administer DEBRE ship calls, times and notifications
termsOfService: "https://www.bsmd.de/" # url to terms page
contact:
name: "Bremen calling API"
url: "https://www.textbausteine.net"
email: "info@textbausteine.net"
license:
name: "Use at your own risk"
url: "https://www.bsmd.de/license"
servers:
# tutorial: https://idratherbewriting.com/learnapidoc/pubapis_openapi_step3_servers_object.html
- url : "https://puls200.dyn-dns.org:8088/brecal/api"
description: "Test server self-hosted by yours truly"
paths:
# tutorial: https://idratherbewriting.com/learnapidoc/pubapis_openapi_step4_paths_object.html
/login:
post:
summary: Returns a JWT session token and user data if successful
requestBody:
description: Login credentials
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/credentials'
responses:
200:
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/login_result'
400:
$ref: '#/components/responses/400'
403:
$ref: '#/components/responses/403'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
/shipcalls:
get:
summary: Gets a list of ship calls
parameters:
- name: participant_id
in: query
required: true
description: "**Id of participant**. *Example: 2*. Id of participant entity requesting ship calls"
schema:
type: integer
responses:
200:
description: ship call list
content:
application/json:
schema:
$ref: '#/components/schemas/shipcalls'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
post:
summary: Create a new ship call
requestBody:
description: Creates a new ship call. **Do not** provide id parameter.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/shipcall'
responses:
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
put:
summary: Updates a ship call
requestBody:
description: Creates a new ship call. The id parameter is **required**.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/shipcall'
responses:
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
/ships:
get:
summary: gets a list of registered shipcalls
responses:
200:
description: list of ships
content:
application/json:
schema:
$ref: '#/components/schemas/ship_list'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
/participant:
get:
summary: gets a particular participant entry corresponding to user id
parameters:
- name: user_id
in: query
required: true
description: "**Id of user**. *Example: 2*. User id returned by verify call."
schema:
type: integer
responses:
200:
description: ship call list
content:
application/json:
schema:
$ref: '#/components/schemas/participant'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
/times:
get:
summary: Get all recorded times for a a ship call
parameters:
- name: shipcall_id
in: query
description: "**Id**. *Example: 42*. Id of referenced ship call."
schema:
type: integer
responses:
200:
description: list of recorded times
content:
application/json:
schema:
$ref: '#/components/schemas/times_list'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
post:
summary: Create a new times entry for a ship call
requestBody:
description: Times entry that will be added to the ship call. **Do not** provide id parameter.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/times'
responses:
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
put:
summary: Update a times entry for a ship call
requestBody:
description: Times entry that will be added to the ship call. The id parameter is **required**.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/times'
responses:
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
delete:
summary: Delete a times entry for a ship call.
parameters:
- name: id
in: query
required: true
schema:
$ref: '#/components/schemas/timesId'
responses:
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
/notifications:
get:
summary: Gets a list of notifications pursuant to a specified participant and ship call
parameters:
- name: participant_id
in: query
required: true
description: "**Id of participant**. *Example: 2*. Id returned through loading of participant"
schema:
type: integer
- name: shipcall_id
in: query
required: true
description: "**Id of ship call**. *Example: 52*. Id given in ship call list"
schema:
$ref: '#/components/schemas/shipcallId'
responses:
200:
description: notification list
content:
application/json:
schema:
$ref: '#/components/schemas/notification'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
/berths:
get:
summary: Gets a list of all berths registered
responses:
200:
description: list of berths
content:
application/json:
schema:
$ref: '#/components/schemas/berth_list'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
500:
$ref: '#/components/responses/500'
503:
$ref: '#/components/responses/503'
components:
schemas:
credentials:
type: object
required:
- username
- password
properties:
username:
format : string
password:
format : string
timesId:
description: The unique identifier for a times entry
type: integer
shipcallId:
description: The unique identifier of a ship call
type: integer
shipcall:
type: object
required:
- id
- ship_id
- type
- eta
- voyage
- etd
properties:
id:
$ref: '#/components/schemas/shipcallId'
type:
type: string
enum:
- incoming
- outgoing
- shifting
description:
type: string
shipcalls:
type: array
items:
$ref: '#/components/schemas/shipcall'
times:
type: object
description: the id parameter needs to be missing on POST and to be present on PUT (Update) calls, otherwise a 400 response will be generated
required:
- shipcall_id
- participant_id
properties:
id:
type: integer
start_planned:
type: string
format: date-time
end_planned:
type: string
format: date-time
duration_planned:
type: integer
start_actual:
type: string
format: date-time
end_actual:
type: string
format: date-time
shipcall_id:
type: integer
participant_id:
type: integer
times_list:
type: array
items:
$ref: '#/components/schemas/times'
berth:
type: object
description: Ship berth used for a ship call
properties:
id:
type: integer
name1:
type: string
name2:
type: string
berth_list:
type: array
items:
$ref: '#/components/schemas/berth'
ship:
type: object
description: a ship
properties:
id:
type: integer
name:
type: string
imo:
type: integer
callsign:
type: string
length:
type: number
format: float
width:
type: number
format: float
created:
type: string
format: date-time
modified:
type: string
format: date-time
ship_list:
type: array
items:
$ref: '#/components/schemas/ship'
notification:
type: object
description: a notification created by the engine if a times entry violates a rule
properties:
id:
type: integer
times_id:
type: integer
participant_id:
type: integer
notification_type:
type: string
enum: [undefined, email, push]
timestamp:
type: string
format: date-time
acknowledged:
type: boolean
notification_list:
type: array
items:
$ref: '#/components/schemas/notification'
participant:
type: object
description: A organisational entity that participates in Bremen Calling
properties:
id:
type: integer
name:
type: string
street:
type: string
postal code:
type: string
city:
type: string
login_result:
type: object
description: result structure of a successful login attempt
properties:
id:
type: integer
participant_id:
type: integer
first_name:
type: string
last_name:
type: string
user_name:
type: string
user_phone:
type: string
exp:
type: float
token:
type: string
Error:
type: object
required:
- message
properties:
message:
description: A human readable error message
type: string
securitySchemes:
ApiKey:
type: apiKey
in: header
name: X-Api-Key
responses:
400:
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
401:
description: Not authorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
500:
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
503:
description: Not available
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
security:
- ApiKey: []
externalDocs:
url: http://textbausteine.net/
description: Extra documentation and conditions for Bremen Calling

140
misc/sample_data.sql Normal file
View File

@ -0,0 +1,140 @@
-- MySQL dump 10.19 Distrib 10.3.38-MariaDB, for debian-linux-gnueabihf (armv8l)
--
-- Host: localhost Database: bremen_calling
-- ------------------------------------------------------
-- Server version 10.3.38-MariaDB-0+deb10u1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Dumping data for table `berth`
--
USE `bremen_calling`;
LOCK TABLES `berth` WRITE;
/*!40000 ALTER TABLE `berth` DISABLE KEYS */;
INSERT INTO `berth` (`id`, `name`) VALUES (1,'Roland Mühle'),(2,'Stahlwerk'),(3,'Kellogs');
/*!40000 ALTER TABLE `berth` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `notification`
--
LOCK TABLES `notification` WRITE;
/*!40000 ALTER TABLE `notification` DISABLE KEYS */;
/*!40000 ALTER TABLE `notification` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `participant`
--
LOCK TABLES `participant` WRITE;
/*!40000 ALTER TABLE `participant` DISABLE KEYS */;
INSERT INTO `participant` (`id`, `name`, `street`, `postal_code`, `city`, `flags`, `created`, `modified`) VALUES (1,'Schick Informatik','Gottlieb-Daimler-Str. 8','73614','Schorndorf',42,'2023-04-17 07:18:19',NULL);
/*!40000 ALTER TABLE `participant` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `role`
--
LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;
INSERT INTO `role` (`id`, `name`, `description`, `created`, `modified`) VALUES (1,'My first role','A very good description','2023-04-17 07:31:57',NULL),(2,'Another role','This role is very nice as well','2023-04-17 07:32:12',NULL);
/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `role_securable_map`
--
LOCK TABLES `role_securable_map` WRITE;
/*!40000 ALTER TABLE `role_securable_map` DISABLE KEYS */;
/*!40000 ALTER TABLE `role_securable_map` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `securable`
--
LOCK TABLES `securable` WRITE;
/*!40000 ALTER TABLE `securable` DISABLE KEYS */;
INSERT INTO `securable` (`id`, `name`, `created`, `modified`) VALUES (1,'First secure thing','2023-04-17 07:38:12',NULL),(2,'Another secure thing','2023-04-17 07:38:22',NULL);
/*!40000 ALTER TABLE `securable` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `ship`
--
LOCK TABLES `ship` WRITE;
/*!40000 ALTER TABLE `ship` DISABLE KEYS */;
/*!40000 ALTER TABLE `ship` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `shipcall`
--
LOCK TABLES `shipcall` WRITE;
/*!40000 ALTER TABLE `shipcall` DISABLE KEYS */;
/*!40000 ALTER TABLE `shipcall` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `shipcall_participant_map`
--
LOCK TABLES `shipcall_participant_map` WRITE;
/*!40000 ALTER TABLE `shipcall_participant_map` DISABLE KEYS */;
/*!40000 ALTER TABLE `shipcall_participant_map` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `times`
--
LOCK TABLES `times` WRITE;
/*!40000 ALTER TABLE `times` DISABLE KEYS */;
/*!40000 ALTER TABLE `times` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `user`
--
LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
INSERT INTO `user` (`id`, `participant_id`, `first_name`, `last_name`, `user_name`, `password_hash`, `api_key`, `created`, `modified`) VALUES (1,1,'Daniel','Schick','dani',NULL,'0815','2023-04-17 07:15:41',NULL);
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Dumping data for table `user_role_map`
--
LOCK TABLES `user_role_map` WRITE;
/*!40000 ALTER TABLE `user_role_map` DISABLE KEYS */;
/*!40000 ALTER TABLE `user_role_map` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2023-04-27 9:09:13

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

9
src/RoleEditor/App.xaml Normal file
View File

@ -0,0 +1,9 @@
<Application x:Class="RoleEditor.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RoleEditor"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace RoleEditor
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

View File

@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@ -0,0 +1,288 @@
// Copyright (c) 2017 schick Informatik
// Description: DataGrid mit etwas "verbesserten" Funktionen
//
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Controls.Primitives;
using System.Collections.Generic;
using brecal.model;
using System.Globalization;
using System.Threading;
namespace RoleEditor
{
/// <summary>
/// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
///
/// Step 1a) Using this custom control in a XAML file that exists in the current project.
/// Add this XmlNamespace attribute to the root element of the markup file where it is
/// to be used:
///
/// xmlns:enictrl="clr-namespace:ENI2.Controls"
///
///
/// Step 1b) Using this custom control in a XAML file that exists in a different project.
/// Add this XmlNamespace attribute to the root element of the markup file where it is
/// to be used:
///
/// xmlns:enictrl="clr-namespace:ENI2.Controls;assembly=ENI2.Controls"
///
/// You will also need to add a project reference from the project where the XAML file lives
/// to this project and Rebuild to avoid compilation errors:
///
/// Right click on the target project in the Solution Explorer and
/// "Add Reference"->"Projects"->[Browse to and select this project]
///
///
/// Step 2)
/// Go ahead and use your control in the XAML file.
///
/// <MyNamespace:ENIDataGrid/>
///
/// </summary>
public class ENIDataGrid : DataGrid
{
/*
static ENIDataGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ENIDataGrid), new FrameworkPropertyMetadata(typeof(ENIDataGrid)));
}
*/
// das hier bildet 1:1 das Kontext-Menü des ANSW ab
public event Action<DbEntity>? EditRequested;
public event Action<DbEntity>? DeleteRequested;
public event Action? CreateRequested;
public event Action? RefreshGrid;
public event Action<DbEntity>? PrintRequested;
public event Action<DbEntity>? ExportRequested;
public event Action<DbEntity>? ShowTextRequested;
public void Initialize()
{
this.MouseDoubleClick += dataGrid_MouseDoubleClick;
this.PreviewKeyDown += ENIDataGrid_PreviewKeyDown;
this.ContextMenu = new ContextMenu();
this.CanUserAddRows = false;
this.IsReadOnly = false;
MenuItem addItem = new MenuItem();
addItem.Header = RoleEditor.Resources.ResourceManager.GetString("textAdd");
addItem.Icon = new Image { Source = Util.LoadImage(RoleEditor.Resources.add) };
addItem.Click += new RoutedEventHandler(this.addItem);
this.ContextMenu.Items.Add(addItem);
MenuItem deleteItem = new MenuItem();
deleteItem.Header = RoleEditor.Resources.ResourceManager.GetString("textDelete");
deleteItem.Icon = new Image { Source = Util.LoadImage(RoleEditor.Resources.delete) };
deleteItem.Click += this.deleteItem;
this.ContextMenu.Items.Add(deleteItem);
MenuItem editItem = new MenuItem();
editItem.Header = RoleEditor.Resources.ResourceManager.GetString("textEdit");
editItem.Icon = new Image { Source = Util.LoadImage(RoleEditor.Resources.edit) };
editItem.Click += this.editItem;
this.ContextMenu.Items.Add(editItem);
/*
this.ContextMenu.Items.Add(new Separator());
MenuItem printItem = new MenuItem();
printItem.Header = RoleEditor.Resources.ResourceManager.GetString("textPrint");
printItem.Icon = new Image { Source = Util.LoadImage(RoleEditor.Resources.printer) };
printItem.Click += this.printItem;
this.ContextMenu.Items.Add(printItem);
MenuItem exportItem = new MenuItem();
exportItem.Header = RoleEditor.Resources.ResourceManager.GetString("textExport");
exportItem.Icon = new Image { Source = Util.LoadImage(RoleEditor.Resources.floppy_disk_blue) };
exportItem.Click += this.exportItem;
this.ContextMenu.Items.Add(exportItem);
MenuItem showTextItem = new MenuItem();
showTextItem.Header = RoleEditor.Resources.ResourceManager.GetString("textShowAsText");
showTextItem.Icon = new Image { Source = Util.LoadImage(RoleEditor.Resources.document_plain) };
showTextItem.Click += this.showTextItem;
this.ContextMenu.Items.Add(showTextItem);
*/
}
private void ENIDataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if(sender is ENIDataGrid)
{
var grid = sender as ENIDataGrid;
if (Key.Delete == e.Key)
this.deleteItem(null, null);
}
}
#region public
public DataGridRow GetRow(int index)
{
DataGridRow row = (DataGridRow)this.ItemContainerGenerator.ContainerFromIndex(index);
if(row == null)
{
this.UpdateLayout();
this.ScrollIntoView(this.Items[index]);
row = (DataGridRow)this.ItemContainerGenerator.ContainerFromIndex(index);
}
return row;
}
public DataGridCell? GetCell(DataGridRow row, int column)
{
if (row != null)
{
DataGridCellsPresenter? presenter = GetVisualChild<DataGridCellsPresenter>(row);
if (presenter == null)
{
this.ScrollIntoView(row, this.Columns[column]);
presenter = GetVisualChild<DataGridCellsPresenter>(row);
}
DataGridCell? cell = (DataGridCell?)presenter?.ItemContainerGenerator.ContainerFromIndex(column);
return cell;
}
return null;
}
public DataGridCell? GetCell(int rowIndex, int columnIndex)
{
DataGridRow row = this.GetRow(rowIndex);
return this.GetCell(row, columnIndex);
}
#endregion
#region protected
protected void addItem(object sender, RoutedEventArgs e)
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us");
if (!this.IsReadOnly)
{
this.CreateRequested?.Invoke();
}
e.Handled = true;
}
protected void deleteItem(object? sender, RoutedEventArgs? e)
{
if((this.SelectedItems != null) && (this.SelectedItems.Count > 0) && !this.IsReadOnly)
{
MessageBoxResult result = MessageBox.Show("Are your sure?", "Please confirm", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
List<DbEntity> deleteList = new List<DbEntity>();
foreach (DbEntity deleteItem in this.SelectedItems)
deleteList.Add(deleteItem);
foreach (DbEntity deleteItem in deleteList)
{
if (deleteItem != null)
{
this.DeleteRequested?.Invoke(deleteItem);
}
}
this.RefreshGrid?.Invoke();
}
}
}
protected void editItem(object sender, RoutedEventArgs e)
{
if((this.SelectedItems != null) && (this.SelectedItems.Count == 1) && !this.IsReadOnly)
{
if (this.SelectedItems[0] is DbEntity selectedEntity)
this.EditRequested?.Invoke(selectedEntity);
}
}
protected void printItem(object sender, RoutedEventArgs e)
{
if ((this.SelectedItems != null) && (this.SelectedItems.Count == 1) )
{
if (this.SelectedItems[0] is DbEntity selectedEntity)
this.PrintRequested?.Invoke(selectedEntity);
}
}
protected void exportItem(object sender, RoutedEventArgs e)
{
if ((this.SelectedItems != null) && (this.SelectedItems.Count == 1))
{
if (this.SelectedItems[0] is DbEntity selectedEntity)
this.ExportRequested?.Invoke(selectedEntity);
}
}
protected void showTextItem(object sender, RoutedEventArgs e)
{
if ((this.SelectedItems != null) && (this.SelectedItems.Count == 1))
{
if (this.SelectedItems[0] is DbEntity selectedEntity)
this.ShowTextRequested?.Invoke(selectedEntity);
}
}
#endregion
#region private
private void dataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender != null)
{
if ((sender is DataGrid grid) && (grid.SelectedItems != null) && (grid.SelectedItems.Count == 1) && !this.IsReadOnly)
{
DataGridRow? dgr = grid.ItemContainerGenerator.ContainerFromItem(grid.SelectedItem) as DataGridRow;
if (grid.SelectedItem is DbEntity selectedEntity)
this.EditRequested?.Invoke(selectedEntity);
}
}
}
#endregion
#region private static
private static T? GetVisualChild<T>(Visual parent) where T : Visual
{
T? child = default;
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual? v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
#endregion
}
}

View File

@ -0,0 +1,41 @@
<Window x:Class="RoleEditor.EditBerthDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:RoleEditor"
mc:Ignorable="d"
Title="Edit berth" Height="160" Width="450" Loaded="Window_Loaded">
<Grid x:Name="berthGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".3*" />
<ColumnDefinition Width=".6*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="*" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<Label Content="Name" HorizontalAlignment="Right" />
<TextBox x:Name="textBoxName" Grid.Column="1" Margin="2" VerticalContentAlignment="Center" Text="{Binding Name, Mode=OneWay}" />
<Label Content="Participant / Terminal" HorizontalAlignment="Right" Grid.Row="1" />
<Grid Grid.Row="1" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="28" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="comboBoxParticipants" Margin="2" SelectedItem="{Binding Participant, Mode=OneWay}" />
<Button x:Name="buttonResetParticipant" Grid.Column="1" Margin="2" Click="buttonResetParticipant_Click">
<Image Source="./Resources/delete2.png"/>
</Button>
</Grid>
<Label Content="Uses lock" HorizontalAlignment="Right" Grid.Row="2" />
<CheckBox x:Name="checkBoxLock" Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" Margin="2" IsChecked="{Binding Path=Lock, Mode=OneWay}"/>
<StackPanel Grid.Column="1" Grid.Row="4" Orientation="Horizontal" FlowDirection="RightToLeft">
<Button x:Name="buttonCancel" Width="80" Content="Cancel" Margin="2" Click="buttonCancel_Click" />
<Button x:Name="buttonOK" Width="80" Content="OK" Margin="2" Click="buttonOK_Click"/>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,51 @@
using brecal.model;
using System.Collections.Generic;
using System.Windows;
namespace RoleEditor
{
/// <summary>
/// Interaction logic for EditBerthDialog.xaml
/// </summary>
public partial class EditBerthDialog : Window
{
public EditBerthDialog()
{
InitializeComponent();
}
public Berth Berth { get; set; } = new Berth();
public List<Participant> Participants { get; } = new List<Participant>();
private void buttonCancel_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
this.Close();
}
private void buttonOK_Click(object sender, RoutedEventArgs e)
{
this.Berth.Name = this.textBoxName.Text.Trim();
this.Berth.Lock = this.checkBoxLock.IsChecked;
this.Berth.Participant = this.comboBoxParticipants.SelectedItem as Participant;
if (this.Berth.Participant != null)
this.Berth.Participant_Id = this.Berth.Participant.Id;
else
this.Berth.Participant_Id = null;
this.DialogResult = true;
this.Close();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = this.Berth;
this.comboBoxParticipants.ItemsSource = this.Participants;
}
private void buttonResetParticipant_Click(object sender, RoutedEventArgs e)
{
this.comboBoxParticipants.SelectedItem = null;
}
}
}

View File

@ -0,0 +1,52 @@
<Window x:Class="RoleEditor.EditShipDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:RoleEditor"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
mc:Ignorable="d"
Title="Edit ship" Height="250" Width="500" Loaded="Window_Loaded">
<Grid x:Name="shipGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".3*" />
<ColumnDefinition Width=".6*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="28" />
<RowDefinition Height="*" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<Label Content="Name" HorizontalAlignment="Right" />
<TextBox x:Name="textBoxName" Grid.Column="1" Margin="2" VerticalContentAlignment="Center" Text="{Binding Name, Mode=OneWay}" />
<Label Content="Participant / Tug company" HorizontalAlignment="Right" Grid.Row="1" />
<Grid Grid.Row="1" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="28" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="comboBoxParticipants" Margin="2" SelectedItem="{Binding Participant, Mode=OneWay}" />
<Button x:Name="buttonResetParticipant" Grid.Column="1" Margin="2" Click="buttonResetParticipant_Click">
<Image Source="./Resources/delete2.png"/>
</Button>
</Grid>
<Label Content="IMO" HorizontalAlignment="Right" Grid.Row="2" />
<xctk:IntegerUpDown Name="integerUpDownIMO" Grid.Column="1" Grid.Row="2" Value="{Binding IMO, Mode=OneWay}" Margin="2" Minimum="1000000" Maximum="9999999"/>
<Label Content="Callsign" HorizontalAlignment="Right" Grid.Row="3" />
<TextBox x:Name="textBoxCallsign" Grid.Column="1" Grid.Row="3" Margin="2" VerticalContentAlignment="Center" Text="{Binding Callsign, Mode=OneWay}" />
<Label Content="Length" HorizontalAlignment="Right" Grid.Row="4" />
<xctk:DoubleUpDown Name="doubleUpDownLength" Grid.Row="4" Grid.Column="1" Value="{Binding Length, Mode=OneWay}" Margin="2" Minimum="0" />
<Label Content="Width" HorizontalAlignment="Right" Grid.Row="5" />
<xctk:DoubleUpDown Name="doubleUpDownWidth" Grid.Row="5" Grid.Column="1" Value="{Binding Width, Mode=OneWay}" Margin="2" Minimum="0"/>
<StackPanel Grid.Column="1" Grid.Row="7" Orientation="Horizontal" FlowDirection="RightToLeft">
<Button x:Name="buttonCancel" Width="80" Content="Cancel" Margin="2" Click="buttonCancel_Click" />
<Button x:Name="buttonOK" Width="80" Content="OK" Margin="2" Click="buttonOK_Click"/>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,55 @@
using brecal.model;
using System.Collections.Generic;
using System.Windows;
namespace RoleEditor
{
/// <summary>
/// Interaction logic for EditShipDialog.xaml
/// </summary>
public partial class EditShipDialog : Window
{
public EditShipDialog()
{
InitializeComponent();
}
public Ship Ship { get; set; } = new Ship();
public List<Participant> Participants { get; } = new List<Participant>();
private void buttonCancel_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
this.Close();
}
private void buttonOK_Click(object sender, RoutedEventArgs e)
{
this.Ship.Name = this.textBoxName.Text.Trim();
this.Ship.Participant = this.comboBoxParticipants.SelectedItem as Participant;
if (this.Ship.Participant != null)
this.Ship.Participant_Id = this.Ship.Participant.Id;
else
this.Ship.Participant_Id = null;
this.Ship.IMO = this.integerUpDownIMO.Value;
this.Ship.Callsign = this.textBoxCallsign.Text.Trim();
this.Ship.Length = this.doubleUpDownLength.Value;
this.Ship.Width = this.doubleUpDownWidth.Value;
this.DialogResult = true;
this.Close();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = this.Ship;
this.comboBoxParticipants.ItemsSource = this.Participants;
}
private void buttonResetParticipant_Click(object sender, RoutedEventArgs e)
{
this.comboBoxParticipants.SelectedItem = null;
}
}
}

View File

@ -0,0 +1,284 @@
<Window x:Class="RoleEditor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:RoleEditor"
mc:Ignorable="d"
Title="Bremen calling admin editor" Height="600" Width="800" Icon="Resources/lock_preferences.ico" Loaded="Window_Loaded">
<Grid>
<TabControl>
<TabItem Header="Participant, users and roles">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=".5*" />
<RowDefinition Height=".5*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width=".5*" />
</Grid.ColumnDefinitions>
<GroupBox Header="Participant" Margin="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160" />
<ColumnDefinition Width=".38*" />
<ColumnDefinition Width=".62*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="listBoxParticipant" Margin="2" Grid.RowSpan="7" SelectionChanged="listBoxParticipant_SelectionChanged">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuItemNewParticipant" Header="New.." Click="menuItemNewParticipant_Click">
<MenuItem.Icon>
<Image Source="Resources/add.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="menuItemDeleteParticipant" Header="Delete" Click="menuItemDeleteParticipant_Click">
<MenuItem.Icon>
<Image Source="Resources/delete2.png" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<Label Content="Name" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Street" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Postal code" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="City" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Active" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Created" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Modified" Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right"/>
<TextBox x:Name="textBoxParticipantName" Grid.Row="0" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxParticipantStreet" Grid.Row="1" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxParticipantPostalCode" Grid.Row="2" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxParticipantCity" Grid.Row="3" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<CheckBox x:Name="checkboxParticipantActive" Grid.Row="4" Grid.Column="2" VerticalAlignment="Center" />
<TextBox x:Name="textBoxParticipantCreated" Grid.Row="5" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxParticipantModified" Grid.Row="6" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<Button x:Name="buttonParticipantSave" Grid.Row="7" Grid.Column="2" Click="buttonParticipantSave_Click" Margin="2">
<DockPanel>
<Image Source="./Resources/disk_blue.png" Margin="0,0,5,0" Height="24" DockPanel.Dock="Left" Width="16"/>
<TextBlock Text="Save" VerticalAlignment="Center" DockPanel.Dock="Right"/>
</DockPanel>
</Button>
</Grid>
</GroupBox>
<GroupBox Header="User" Margin="2" Grid.Row="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160" />
<ColumnDefinition Width=".38*" />
<ColumnDefinition Width=".62*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="listBoxUser" Margin="2" Grid.RowSpan="9" SelectionChanged="listBoxUser_SelectionChanged">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuItemNewUser" Header="New.." Click="menuItemNewUser_Click">
<MenuItem.Icon>
<Image Source="Resources/add.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="menuItemDeleteUser" Header="Delete" Click="menuItemDeleteUser_Click">
<MenuItem.Icon>
<Image Source="Resources/delete2.png" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<Label Grid.Row="0" Grid.Column="1" Content="First name" HorizontalAlignment="Right"/>
<Label Grid.Row="1" Grid.Column="1" Content="Last name" HorizontalAlignment="Right"/>
<Label Grid.Row="2" Grid.Column="1" Content="User name" HorizontalAlignment="Right"/>
<Label Grid.Row="3" Grid.Column="1" Content="Password" HorizontalAlignment="Right"/>
<Label Grid.Row="4" Grid.Column="1" Content="API Key" HorizontalAlignment="Right"/>
<Label Content="Created" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Modified" Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right"/>
<TextBox x:Name="textBoxUserFirstName" Grid.Row="0" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxUserLastName" Grid.Row="1" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxUserUserName" Grid.Row="2" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxUserPassword" Grid.Row="3" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxUserAPIKey" Grid.Row="4" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxUserCreated" Grid.Row="5" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxUserModified" Grid.Row="6" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<Button x:Name="buttonUserSave" Grid.Row="7" Grid.Column="2" Click="buttonUserSave_Click" Margin="2">
<DockPanel>
<Image Source="./Resources/disk_blue.png" Margin="0,0,5,0" Height="24" DockPanel.Dock="Left" Width="16"/>
<TextBlock Text="Save" VerticalAlignment="Center" DockPanel.Dock="Right"/>
</DockPanel>
</Button>
</Grid>
</GroupBox>
<GroupBox Header="Role" Margin="2" Grid.Column="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="*"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width="60" />
<ColumnDefinition Width=".5*" />
</Grid.ColumnDefinitions>
<Label Content="User roles" Grid.Row="0" Grid.Column="0" />
<Label Content="All roles" Grid.Row="0" Grid.Column="2" />
<ListBox x:Name="listBoxUserRoles" Grid.Row="1" Grid.Column="0" Grid.RowSpan="3" Margin="2" />
<ListBox x:Name="listBoxRoles" Grid.Row="1" Grid.Column="2" Grid.RowSpan="3" Margin="2" SelectionChanged="listBoxRoles_SelectionChanged">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuItemNewRole" Header="New.." Click="menuItemNewRole_Click">
<MenuItem.Icon>
<Image Source="Resources/add.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="menuItemDeleteRole" Header="Delete" Click="menuItemDeleteRole_Click">
<MenuItem.Icon>
<Image Source="Resources/delete2.png" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<Button x:Name="buttonAddRole" Margin="2" Grid.Row="1" Grid.Column="1" Click="buttonAddRole_Click">
<Image Source="./Resources/arrow_left_green.png"/>
</Button>
<Button x:Name="buttonRemoveRole" Margin="2" Grid.Row="2" Grid.Column="1" Click="buttonRemoveRole_Click">
<Image Source="./Resources/delete2.png"/>
</Button>
<Label Content="Name" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right" />
<Label Content="Descr." Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right" />
<TextBox x:Name="textBoxRoleName" Grid.Row="4" Grid.Column="2" Margin="2" VerticalContentAlignment="Center"/>
<TextBox x:Name="textBoxRoleDescription" Grid.Row="5" Grid.Column="2" Grid.RowSpan="2" Margin="2" VerticalContentAlignment="Top"/>
<Button x:Name="buttonSaveRole" Grid.Row="7" Grid.Column="2" Margin="2" Click="buttonSaveRole_Click">
<DockPanel>
<Image Source="./Resources/disk_blue.png" Margin="0,0,5,0" Height="24" DockPanel.Dock="Left" Width="16"/>
<TextBlock Text="Save" VerticalAlignment="Center" DockPanel.Dock="Right"/>
</DockPanel>
</Button>
</Grid>
</GroupBox>
<GroupBox Header="Securable" Margin="2" Grid.Row="1" Grid.Column="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
<RowDefinition Height="*"/>
<RowDefinition Height="28"/>
<RowDefinition Height="28"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width="60" />
<ColumnDefinition Width=".5*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Role securables" />
<Label Grid.Row="0" Grid.Column="2" Content="Securables" />
<ListBox x:Name="listBoxRoleSecurables" Grid.Row="1" Grid.Column="0" Grid.RowSpan="3" Margin="2" />
<ListBox x:Name="listBoxSecurables" Grid.Row="1" Grid.Column="2" Grid.RowSpan="3" Margin="2" SelectionChanged="listBoxSecurables_SelectionChanged">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem x:Name="menuItemNewSecurable" Header="New.." Click="menuItemNewSecurable_Click">
<MenuItem.Icon>
<Image Source="Resources/add.png" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="menuItemDeleteSecurable" Header="Delete" Click="menuItemDeleteSecurable_Click">
<MenuItem.Icon>
<Image Source="Resources/delete2.png" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<Button x:Name="buttonAddSecurable" Margin="2" Grid.Row="1" Grid.Column="1" Click="buttonAddSecurable_Click">
<Image Source="./Resources/arrow_left_green.png"/>
</Button>
<Button x:Name="buttonRemoveSecurable" Margin="2" Grid.Row="2" Grid.Column="1" Click="buttonRemoveSecurable_Click">
<Image Source="./Resources/delete2.png"/>
</Button>
<Label Content="Name" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right" />
<TextBox x:Name="textBoxSecurableName" Grid.Row="4" Grid.Column="2" Margin="2" VerticalContentAlignment="Center"/>
<Button x:Name="buttonSaveSecurable" Grid.Row="5" Grid.Column="2" Margin="2" Click="buttonSaveSecurable_Click">
<DockPanel>
<Image Source="./Resources/disk_blue.png" Margin="0,0,5,0" Height="24" DockPanel.Dock="Left" Width="16"/>
<TextBlock Text="Save" VerticalAlignment="Center" DockPanel.Dock="Right"/>
</DockPanel>
</Button>
</Grid>
</GroupBox>
</Grid>
</TabItem>
<TabItem Header="Berths">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Content="Import excel" Margin="2" x:Name="buttonImportBerths" Click="buttonImportBerths_Click" Width="100" HorizontalAlignment="Left" Grid.Row="0"/>
<local:ENIDataGrid x:Name="dataGridBerths" Grid.Row="1" SelectionMode="Single" IsReadOnly="True" AlternatingRowBackground="LightBlue" AutoGenerateColumns="False"
CanUserAddRows="False" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<DataGrid.Columns>
<DataGridTextColumn Header="Id" Binding="{Binding Path=Id}" IsReadOnly="True"/>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" IsReadOnly="True"/>
<DataGridCheckBoxColumn Header="Lock" Binding="{Binding Path=Lock}" IsReadOnly="True"/>
<DataGridTextColumn Header="Terminal" Binding="{Binding Path=Terminal, Mode=OneWay}" IsReadOnly="True"/>
</DataGrid.Columns>
</local:ENIDataGrid>
</Grid>
</TabItem>
<TabItem Header="Ships">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Content="Import excel" Margin="2" x:Name="buttonImportShipss" Click="buttonImportShipss_Click" Width="100" HorizontalAlignment="Left" Grid.Row="0"/>
<local:ENIDataGrid x:Name="dataGridShips" Grid.Row="1" SelectionMode="Single" IsReadOnly="True" AlternatingRowBackground="LightBlue" AutoGenerateColumns="False"
CanUserAddRows="False" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<DataGrid.Columns>
<DataGridTextColumn Header="Id" Binding="{Binding Path=Id}" IsReadOnly="True"/>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" IsReadOnly="True"/>
<DataGridCheckBoxColumn Header="Tug" Binding="{Binding Path=IsTug, Mode=OneWay}" IsReadOnly="True"/>
<DataGridTextColumn Header="Tug company" Binding="{Binding Path=TugCompany, Mode=OneWay}" IsReadOnly="True"/>
<DataGridTextColumn Header="IMO" Binding="{Binding Path=IMO}" IsReadOnly="True"/>
<DataGridTextColumn Header="Callsign" Binding="{Binding Path=Callsign}" IsReadOnly="True"/>
<DataGridTextColumn Header="Length" Binding="{Binding Path=Length}" IsReadOnly="True"/>
<DataGridTextColumn Header="Width" Binding="{Binding Path=Width}" IsReadOnly="True"/>
</DataGrid.Columns>
</local:ENIDataGrid>
</Grid>
</TabItem>
</TabControl>
</Grid>
</Window>

View File

@ -0,0 +1,574 @@

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using brecal.model;
using brecal.mysql;
namespace RoleEditor
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
#region private fields
private readonly ObservableCollection<Participant> _participants = new ObservableCollection<Participant>();
private readonly ObservableCollection<Role> _roles = new ObservableCollection<Role>();
private readonly ObservableCollection<Securable> _securables = new ObservableCollection<Securable>();
private readonly ObservableCollection<User> _users = new ObservableCollection<User>();
private readonly ObservableCollection<RoleAssignment> _assignedRoles = new ObservableCollection<RoleAssignment>();
private readonly ObservableCollection<SecurableAssignment> _assignedSecurables = new ObservableCollection<SecurableAssignment>();
private readonly ObservableCollection<Berth> _berths = new ObservableCollection<Berth>();
private readonly ObservableCollection<Ship> _ships = new ObservableCollection<Ship>();
private DBManager _dbManager;
#endregion
#region Construction / Loading
public MainWindow()
{
InitializeComponent();
_dbManager = new();
}
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
// try database connection
try
{
// load all participants
List<Participant> participants = await Participant.LoadAll(_dbManager);
foreach(Participant p in participants)
_participants.Add(p);
this.listBoxParticipant.ItemsSource = _participants;
// load all roles
foreach(Role r in await Role.LoadAll(_dbManager))
_roles.Add(r);
this.listBoxRoles.ItemsSource = _roles;
// load all securables
foreach(Securable s in await Securable.LoadAll(_dbManager))
_securables.Add(s);
this.listBoxSecurables.ItemsSource = _securables;
// load all berths
foreach (Berth b in await Berth.LoadAll(_dbManager))
{
_berths.Add(b);
if(b.Participant_Id != null)
{
b.Participant = participants.Where( p => p.Id== b.Participant_Id ).FirstOrDefault();
}
}
this.dataGridBerths.Initialize();
this.dataGridBerths.ItemsSource = _berths;
this.dataGridBerths.CreateRequested += DataGridBerths_CreateRequested;
this.dataGridBerths.EditRequested += DataGridBerths_EditRequested;
this.dataGridBerths.DeleteRequested += DataGridBerths_DeleteRequested;
// load all ships
foreach (Ship s in await Ship.LoadAll(_dbManager))
{
_ships.Add(s);
if(s.Participant_Id != null)
{
s.Participant = participants.Where( p => p.Id == s.Participant_Id ).FirstOrDefault();
}
}
this.dataGridShips.Initialize();
this.dataGridShips.ItemsSource = _ships;
this.dataGridShips.CreateRequested += DataGridShips_CreateRequested;
this.dataGridShips.EditRequested += DataGridShips_EditRequested;
this.dataGridShips.DeleteRequested += DataGridShips_DeleteRequested;
// set other item sources (filled later after selection)
this.listBoxUser.ItemsSource = _users;
this.listBoxRoleSecurables.ItemsSource = _assignedSecurables;
this.listBoxUserRoles.ItemsSource = _assignedRoles;
}
catch (Exception ex)
{
MessageBox.Show($"Database connection couldn't be established: {ex.Message}");
this.Close();
}
}
#region ship context menu callbacks
private void DataGridShips_DeleteRequested(DbEntity obj)
{
if(obj is Ship s)
this._ships.Remove(s);
}
private async void DataGridShips_EditRequested(DbEntity obj)
{
if(obj is Ship s)
{
EditShipDialog esd = new();
esd.Ship = s;
esd.Participants.AddRange(this._participants);
if (esd.ShowDialog() ?? false)
{
await s.Save(_dbManager);
this.dataGridShips.ItemsSource = null;
this.dataGridShips.ItemsSource = _ships;
}
}
}
private async void DataGridShips_CreateRequested()
{
Ship s = new();
EditShipDialog esd = new();
esd.Ship = s;
esd.Participants.AddRange(this._participants);
if(esd.ShowDialog() ?? false)
{
_ships.Add(s);
await s.Save(_dbManager);
this.dataGridShips.ItemsSource = null;
this.dataGridShips.ItemsSource = _ships;
}
}
#endregion
#region berth context menu callbacks
private void DataGridBerths_DeleteRequested(DbEntity obj)
{
if(obj is Berth b)
this._berths.Remove(b);
}
private async void DataGridBerths_EditRequested(DbEntity obj)
{
if(obj is Berth b)
{
EditBerthDialog ebd = new();
ebd.Berth = b;
ebd.Participants.AddRange(this._participants);
if (ebd.ShowDialog() ?? false)
{
await b.Save(_dbManager);
this.dataGridBerths.ItemsSource = null; // extreme stupidity for me not wanting to implement INotifyPropertyChanged
this.dataGridBerths.ItemsSource = _berths;
}
}
}
private async void DataGridBerths_CreateRequested()
{
Berth b = new();
EditBerthDialog ebd = new();
ebd.Berth = b;
ebd.Participants.AddRange(this._participants);
if (ebd.ShowDialog() ?? false)
{
_berths.Add(b);
await b.Save(_dbManager);
this.dataGridBerths.ItemsSource = null;
this.dataGridBerths.ItemsSource = _berths;
}
}
#endregion
#endregion
#region button callbacks
private async void buttonParticipantSave_Click(object sender, RoutedEventArgs e)
{
Participant? p = this.listBoxParticipant.SelectedItem as Participant;
if (p != null)
{
p.Name = this.textBoxParticipantName.Text.Trim();
p.Street = this.textBoxParticipantStreet.Text.Trim();
p.PostalCode = this.textBoxParticipantPostalCode.Text.Trim();
p.City = this.textBoxParticipantCity.Text.Trim();
await p.Save(_dbManager);
this.listBoxParticipant.ItemsSource = null;
this.listBoxParticipant.ItemsSource = _users;
this.listBoxParticipant.SelectedItem = p;
}
}
private async void buttonUserSave_Click(object sender, RoutedEventArgs e)
{
User? u = this.listBoxUser.SelectedItem as User;
if(u != null)
{
u.Firstname = this.textBoxUserFirstName.Text.Trim();
u.Lastname = this.textBoxUserLastName.Text.Trim();
u.Username = this.textBoxUserUserName.Text.Trim();
if(this.textBoxUserPassword.Text.Trim().Length > 0 )
{
var data = Encoding.UTF8.GetBytes(this.textBoxUserPassword.Text.Trim());
using SHA512 sha = SHA512.Create();
byte[] hashedInputBytes = sha.ComputeHash(data);
var hashedInputStringBuilder = new StringBuilder(128);
foreach (var b in hashedInputBytes)
hashedInputStringBuilder.Append(b.ToString("X2"));
u.PasswordHash = hashedInputStringBuilder.ToString();
}
u.APIKey = this.textBoxUserAPIKey.Text.Trim();
await u.Save(_dbManager);
this.listBoxUser.ItemsSource = null;
this.listBoxUser.ItemsSource = _users;
this.listBoxUser.SelectedItem = u;
}
}
private async void buttonAddRole_Click(object sender, RoutedEventArgs e)
{
Role? r = this.listBoxRoles.SelectedItem as Role;
User? u = this.listBoxUser.SelectedItem as User;
if((r != null) && (u != null))
{
// test if assignment is already present
bool foundMatchingAssignment = false;
foreach(RoleAssignment ra in _assignedRoles)
{
if((ra.UserId == u.Id) && (ra.RoleId == r.Id))
{
foundMatchingAssignment = true;
break;
}
}
if(!foundMatchingAssignment)
{
RoleAssignment ra = new RoleAssignment();
ra.UserId = (int) u.Id;
ra.RoleId = (int) r.Id;
ra.AssignedRole = r;
ra.AssignedUser = u;
await ra.Save(_dbManager);
_assignedRoles.Add(ra);
}
}
}
private async void buttonRemoveRole_Click(object sender, RoutedEventArgs e)
{
// remove role from user
RoleAssignment? ra = this.listBoxUserRoles.SelectedItem as RoleAssignment;
if(ra != null)
{
await ra.Delete(_dbManager);
if(_assignedRoles.Contains(ra))
_assignedRoles.Remove(ra);
}
}
private async void buttonAddSecurable_Click(object sender, RoutedEventArgs e)
{
if ((this.listBoxRoles.SelectedItem is Role r) && (this.listBoxSecurables.SelectedItem is Securable s))
{
// test if assignment is already present
bool foundMatchingAssignment = false;
foreach (SecurableAssignment sa in _assignedSecurables)
{
if ((sa.SecurableId == s.Id) && (sa.RoleId == r.Id))
{
foundMatchingAssignment = true;
break;
}
}
if (!foundMatchingAssignment)
{
SecurableAssignment sa = new SecurableAssignment();
sa.SecurableId = (int)s.Id;
sa.RoleId = (int)r.Id;
sa.AssignedRole = r;
sa.AssignedSecurable = s;
await sa.Save(_dbManager);
_assignedSecurables.Add(sa);
}
}
}
private async void buttonRemoveSecurable_Click(object sender, RoutedEventArgs e)
{
SecurableAssignment? sa = this.listBoxRoleSecurables.SelectedItem as SecurableAssignment;
if(sa != null)
{
await sa.Delete(_dbManager);
if (_assignedSecurables.Contains(sa))
_assignedSecurables.Remove(sa);
}
}
private async void buttonSaveSecurable_Click(object sender, RoutedEventArgs e)
{
Securable? s = this.listBoxSecurables.SelectedItem as Securable;
if(s != null)
{
s.Name = this.textBoxSecurableName.Text.Trim();
await s.Save(_dbManager);
this.listBoxSecurables.ItemsSource = null;
this.listBoxSecurables.ItemsSource = _securables;
this.listBoxSecurables.SelectedItem = s;
}
}
private async void buttonSaveRole_Click(object sender, RoutedEventArgs e)
{
Role? r = this.listBoxRoles.SelectedItem as Role;
if(r != null)
{
r.Name = this.textBoxRoleName.Text.Trim();
r.Description = this.textBoxRoleDescription.Text.Trim();
await r.Save(_dbManager);
this.listBoxRoles.ItemsSource = null;
this.listBoxRoles.ItemsSource = _roles;
this.listBoxRoles.SelectedItem = r;
}
}
#endregion
#region listbox selection callbacks
private async void listBoxParticipant_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Participant? p = this.listBoxParticipant.SelectedItem as Participant;
this.textBoxParticipantName.Text = (p != null) ? p.Name : string.Empty;
this.textBoxParticipantStreet.Text = (p != null) ? p.Street : string.Empty;
this.textBoxParticipantPostalCode.Text = (p != null) ? p.PostalCode : string.Empty;
this.textBoxParticipantCity.Text = (p != null) ? p.City : string.Empty;
// this.checkboxParticipantActive.Checked = (p != null) ? p.
this.textBoxParticipantCreated.Text = (p != null) ? p.Created.ToString() : string.Empty;
this.textBoxParticipantModified.Text = (p != null) ? p.Modified.ToString() : string.Empty;
// -> load users for this participant selection
this._users.Clear();
foreach (User u in await User.LoadForParticipant(p, _dbManager))
_users.Add(u);
}
private async void listBoxRoles_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Role? r = this.listBoxRoles.SelectedItem as Role;
this.textBoxRoleName.Text = (r != null) ? r.Name : string.Empty;
this.textBoxRoleDescription.Text = (r != null) ? r.Description : string.Empty;
_assignedSecurables.Clear();
if (r != null)
{
// load assigned securables
foreach (SecurableAssignment sa in await SecurableAssignment.LoadForRole(r, _dbManager))
{
foreach (Securable s in this._securables)
{
if (sa.SecurableId == s.Id)
{
sa.AssignedSecurable = s;
break;
}
}
_assignedSecurables.Add(sa);
}
}
}
private async void listBoxUser_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
User? u = this.listBoxUser.SelectedItem as User;
this.textBoxUserFirstName.Text = (u != null) ? u.Firstname : string.Empty;
this.textBoxUserLastName.Text = (u != null) ? u.Lastname : string.Empty;
this.textBoxUserUserName.Text = (u != null) ? u.Username : string.Empty;
this.textBoxUserAPIKey.Text = (u != null) ? u.APIKey : string.Empty;
this.textBoxUserCreated.Text = (u != null) ? u.Created.ToString() : string.Empty;
this.textBoxUserModified.Text = (u != null) ? u.Modified.ToString() : string.Empty;
this.textBoxUserPassword.Text = string.Empty;
_assignedRoles.Clear();
if (u != null)
{
// load roles assigned to user
foreach (RoleAssignment ra in await RoleAssignment.LoadForUser(u, _dbManager))
{
foreach (Role r in this._roles)
{
if (ra.RoleId == r.Id)
{
ra.AssignedRole = r;
break;
}
}
_assignedRoles.Add(ra);
}
}
}
private void listBoxSecurables_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Securable? s = this.listBoxSecurables.SelectedItem as Securable;
this.textBoxSecurableName.Text = (s != null) ? s.Name : string.Empty;
}
#endregion
#region menuitem callbacks
private async void menuItemDeleteParticipant_Click(object sender, RoutedEventArgs e)
{
try
{
if(this.listBoxParticipant.SelectedItem is Participant p)
{
await p.Delete(_dbManager);
this._participants.Remove(p);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void menuItemNewParticipant_Click(object sender, RoutedEventArgs e)
{
Participant p = new();
this._participants.Add(p);
this.listBoxParticipant.SelectedItem = p;
}
private void menuItemNewUser_Click(object sender, RoutedEventArgs e)
{
Participant? p = this.listBoxParticipant.SelectedItem as Participant;
if(p != null)
{
User u = new();
u.Participant_Id = p.Id;
_users.Add(u);
this.listBoxUser.SelectedItem = u;
}
}
private async void menuItemDeleteUser_Click(object sender, RoutedEventArgs e)
{
try
{
if (this.listBoxUser.SelectedItem is User u)
{
await u.Delete(_dbManager);
this._users.Remove(u);
}
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void menuItemNewRole_Click(object sender, RoutedEventArgs e)
{
Role r = new();
this._roles.Add(r);
this.listBoxRoles.SelectedItem = r;
}
private async void menuItemDeleteRole_Click(object sender, RoutedEventArgs e)
{
try
{
if (this.listBoxRoles.SelectedItem is Role r)
{
await r.Delete(_dbManager);
this._roles.Remove(r);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void menuItemNewSecurable_Click(object sender, RoutedEventArgs e)
{
Securable s = new Securable();
_securables.Add(s);
this.listBoxSecurables.SelectedItem = s;
}
private async void menuItemDeleteSecurable_Click(object sender, RoutedEventArgs e)
{
try
{
if (this.listBoxSecurables.SelectedItem is Securable s)
{
await s.Delete(_dbManager);
this._securables.Remove(s);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#endregion
#region Excel import
private void buttonImportBerths_Click(object sender, RoutedEventArgs e)
{
}
private void buttonImportShipss_Click(object sender, RoutedEventArgs e)
{
}
#endregion
#region private static helper
private static BitmapImage? LoadImage(byte[] imageData)
{
if (imageData == null || imageData.Length == 0) return null;
var image = new BitmapImage();
using (var mem = new MemoryStream(imageData))
{
mem.Position = 0;
image.BeginInit();
image.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = null;
image.StreamSource = mem;
image.EndInit();
}
image.Freeze();
return image;
}
#endregion
}
}

277
src/RoleEditor/Resources.Designer.cs generated Normal file
View File

@ -0,0 +1,277 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace RoleEditor {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("RoleEditor.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] about {
get {
object obj = ResourceManager.GetObject("about", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] add {
get {
object obj = ResourceManager.GetObject("add", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] arrow_left_green {
get {
object obj = ResourceManager.GetObject("arrow_left_green", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] businessman {
get {
object obj = ResourceManager.GetObject("businessman", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] delete {
get {
object obj = ResourceManager.GetObject("delete", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] delete2 {
get {
object obj = ResourceManager.GetObject("delete2", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] disk_blue {
get {
object obj = ResourceManager.GetObject("disk_blue", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] document_plain {
get {
object obj = ResourceManager.GetObject("document_plain", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] edit {
get {
object obj = ResourceManager.GetObject("edit", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] floppy_disk_blue {
get {
object obj = ResourceManager.GetObject("floppy_disk_blue", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] id_card {
get {
object obj = ResourceManager.GetObject("id_card", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] key1 {
get {
object obj = ResourceManager.GetObject("key1", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] key1_add {
get {
object obj = ResourceManager.GetObject("key1_add", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] lock_preferences {
get {
object obj = ResourceManager.GetObject("lock_preferences", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] printer {
get {
object obj = ResourceManager.GetObject("printer", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] safe {
get {
object obj = ResourceManager.GetObject("safe", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized string similar to Add.
/// </summary>
internal static string textAdd {
get {
return ResourceManager.GetString("textAdd", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
internal static string textDelete {
get {
return ResourceManager.GetString("textDelete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
internal static string textEdit {
get {
return ResourceManager.GetString("textEdit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Export.
/// </summary>
internal static string textExport {
get {
return ResourceManager.GetString("textExport", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Print.
/// </summary>
internal static string textPrint {
get {
return ResourceManager.GetString("textPrint", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to _Show as text.
/// </summary>
internal static string textShowAsText {
get {
return ResourceManager.GetString("textShowAsText", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="about" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\about.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="add" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\add.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="arrow_left_green" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\arrow_left_green.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="businessman" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\businessman.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="delete" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\delete.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="delete2" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\delete2.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="disk_blue" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\disk_blue.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="document_plain" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\document_plain.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="edit" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\edit.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="floppy_disk_blue" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\floppy_disk_blue.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="id_card" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\id_card.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="key1" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\key1.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="key1_add" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\key1_add.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="lock_preferences" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\lock_preferences.ico;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="printer" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\printer.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="safe" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\safe.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="textAdd" xml:space="preserve">
<value>Add</value>
</data>
<data name="textDelete" xml:space="preserve">
<value>Delete</value>
</data>
<data name="textEdit" xml:space="preserve">
<value>Edit</value>
</data>
<data name="textExport" xml:space="preserve">
<value>Export</value>
</data>
<data name="textPrint" xml:space="preserve">
<value>Print</value>
</data>
<data name="textShowAsText" xml:space="preserve">
<value>_Show as text</value>
</data>
</root>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,75 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ApplicationIcon>Resources\lock_preferences.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\about.png" />
<None Remove="Resources\add.png" />
<None Remove="Resources\arrow_left_green.png" />
<None Remove="Resources\businessman.png" />
<None Remove="Resources\delete.png" />
<None Remove="Resources\delete2.png" />
<None Remove="Resources\disk_blue.png" />
<None Remove="Resources\document_plain.png" />
<None Remove="Resources\edit.png" />
<None Remove="Resources\floppy_disk_blue.png" />
<None Remove="Resources\id_card.png" />
<None Remove="Resources\key1.png" />
<None Remove="Resources\key1_add.png" />
<None Remove="Resources\printer.png" />
<None Remove="Resources\safe.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\brecal.model\brecal.model.csproj" />
<ProjectReference Include="..\brecal.mysql\brecal.mysql.csproj" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources/lock_preferences.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\about.png" />
<Resource Include="Resources\add.png" />
<Resource Include="Resources\arrow_left_green.png" />
<Resource Include="Resources\businessman.png" />
<Resource Include="Resources\delete.png" />
<Resource Include="Resources\delete2.png" />
<Resource Include="Resources\disk_blue.png" />
<Resource Include="Resources\document_plain.png" />
<Resource Include="Resources\edit.png" />
<Resource Include="Resources\floppy_disk_blue.png" />
<Resource Include="Resources\id_card.png" />
<Resource Include="Resources\key1.png" />
<Resource Include="Resources\key1_add.png" />
<Resource Include="Resources\printer.png" />
<Resource Include="Resources\safe.png" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33516.290
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoleEditor", "RoleEditor.csproj", "{8A8CB0D3-7728-468E-AB11-E811BA5B5BC0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "brecal.model", "..\brecal.model\brecal.model.csproj", "{F3BC5ADC-BF57-47DC-A5D5-CC4A13857DEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "brecal.mysql", "..\brecal.mysql\brecal.mysql.csproj", "{E88F908B-48C9-46BD-A3AE-C36FBE9EDF1F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8A8CB0D3-7728-468E-AB11-E811BA5B5BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A8CB0D3-7728-468E-AB11-E811BA5B5BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A8CB0D3-7728-468E-AB11-E811BA5B5BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A8CB0D3-7728-468E-AB11-E811BA5B5BC0}.Release|Any CPU.Build.0 = Release|Any CPU
{F3BC5ADC-BF57-47DC-A5D5-CC4A13857DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3BC5ADC-BF57-47DC-A5D5-CC4A13857DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3BC5ADC-BF57-47DC-A5D5-CC4A13857DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3BC5ADC-BF57-47DC-A5D5-CC4A13857DEE}.Release|Any CPU.Build.0 = Release|Any CPU
{E88F908B-48C9-46BD-A3AE-C36FBE9EDF1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E88F908B-48C9-46BD-A3AE-C36FBE9EDF1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E88F908B-48C9-46BD-A3AE-C36FBE9EDF1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E88F908B-48C9-46BD-A3AE-C36FBE9EDF1F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {12E46C08-C5A8-46E8-8A8C-04BA6F197DCB}
EndGlobalSection
EndGlobal

31
src/RoleEditor/Util.cs Normal file
View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
namespace RoleEditor
{
public static class Util
{
public static BitmapImage? LoadImage (byte[] imageData)
{
if (imageData == null || imageData.Length == 0) return null;
var image = new BitmapImage();
using (var mem = new MemoryStream(imageData))
{
mem.Position = 0;
image.BeginInit();
image.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = null;
image.StreamSource = mem;
image.EndInit();
}
image.Freeze();
return image;
}
}
}

116
src/brecal.model/Berth.cs Normal file
View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace brecal.model
{
public class Berth : DbEntity
{
#region Properties
public string? Name { get; set; }
public bool? Lock { get; set; }
public uint? Participant_Id { get; set; }
public Participant? Participant { get; set; }
public string? Terminal { get { if (Participant != null) return Participant.Name; else return "n/a"; } }
#endregion
#region public static methods
public static async Task<List<Berth>> LoadAll(IDBManager manager)
{
List<DbEntity> loadResultList = await manager.Load(SetLoadQuery, LoadElems);
List<Berth> result = new();
foreach (Berth b in loadResultList.Cast<Berth>())
result.Add(b);
return result;
}
public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
{
cmd.CommandText = "SELECT id, name, participant_id, `lock`, created, modified FROM berth";
}
public static List<DbEntity> LoadElems(IDataReader reader)
{
List<DbEntity> result = new List<DbEntity>();
while (reader.Read())
{
Berth b = new();
b.Id = (uint)reader.GetInt32(0);
if (!reader.IsDBNull(1)) b.Name = reader.GetString(1);
if (!reader.IsDBNull(2)) b.Participant_Id = (uint) reader.GetInt32(2);
if (!reader.IsDBNull(3)) b.Lock = reader.GetBoolean(3);
if (!reader.IsDBNull(4)) b.Created = reader.GetDateTime(4);
if (!reader.IsDBNull(5)) b.Modified = reader.GetDateTime(5);
result.Add(b);
}
return result;
}
#endregion
#region DbEntity implementation
public override void SetCreate(IDbCommand cmd)
{
cmd.CommandText = "INSERT INTO berth (participant_id, name, `lock`) VALUES ( @PID, @NAME, @LOCK)";
this.SetParameters(cmd);
}
public override void SetDelete(IDbCommand cmd)
{
cmd.CommandText = "DELETE FROM berth WHERE id = @ID";
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
public override void SetUpdate(IDbCommand cmd)
{
cmd.CommandText = "UPDATE berth SET name = @NAME, participant_id = @PID, `lock` = @LOCK WHERE id = @ID";
this.SetParameters(cmd);
}
#endregion
#region private methods
private void SetParameters(IDbCommand cmd)
{
IDbDataParameter id = cmd.CreateParameter();
id.ParameterName = "ID";
id.Value = this.Id;
cmd.Parameters.Add(id);
IDbDataParameter pid = cmd.CreateParameter();
pid.ParameterName = "PID";
pid.Value = this.Participant_Id;
cmd.Parameters.Add(pid);
IDbDataParameter name = cmd.CreateParameter();
name.ParameterName = "NAME";
name.Value = this.Name;
cmd.Parameters.Add(name);
IDbDataParameter lockparam = cmd.CreateParameter();
lockparam.ParameterName = "LOCK";
lockparam.Value = this.Lock;
cmd.Parameters.Add(lockparam);
}
#endregion
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace brecal.model
{
public abstract class DbEntity
{
/// <summary>
/// DB primary key
/// </summary>
public uint Id { get; set; }
/// <summary>
/// Creation timestamp, if null record is unsaved
/// </summary>
public DateTime? Created { get; set; }
/// <summary>
/// Modified timestamp, if null record was never modified
/// </summary>
public DateTime? Modified { get; set; }
/// <summary>
/// Set query and cmd parameters for an update query
/// </summary>
/// <param name="cmd">CMD created by DB manager</param>
public abstract void SetUpdate(IDbCommand cmd);
/// <summary>
/// set query and cmd parameters for a create query
/// </summary>
/// <param name="cmd">CMD created by DB manager</param>
public abstract void SetCreate(IDbCommand cmd);
/// <summary>
/// set query and cmd parameters for a delete query
/// </summary>
/// <param name="cmd">CMD created by DB manager</param>
public abstract void SetDelete(IDbCommand cmd);
/// <summary>
/// Each database entity must be able to save itself to the database
/// </summary>
public async Task Save(IDBManager manager)
{
if (this.Created.HasValue)
{
await manager.ExecuteNonQuery(this.SetUpdate);
}
else
{
this.Id = (uint)await manager.ExecuteNonQuery(this.SetCreate);
}
}
/// <summary>
/// Each entity must be able to delete itself
/// </summary>
public async Task Delete(IDBManager manager)
{
await manager.ExecuteNonQuery(this.SetDelete);
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace brecal.model
{
public interface IDBManager
{
delegate List<DbEntity> LoadFunc<T>(T entity);
delegate void QueryFunc(IDbCommand cmd, params object?[] args);
Task<List<DbEntity>> Load(QueryFunc prepareAction, LoadFunc<IDataReader> loadAction, params object?[] args);
Task<object?> ExecuteScalar(Action<IDbCommand> prepareAction);
Task<int> ExecuteNonQuery(Action<IDbCommand> prepareAction);
}
}

View File

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading.Tasks;
namespace brecal.model
{
public class Participant : DbEntity
{
#region Enumerations
[Flags]
public enum ParticipantType
{
NONE = 0,
BSMD = 1,
TERMINAL = 2,
PILOT = 4,
AGENCY = 8,
MOORING = 16,
PORT_ADMINISTRATION = 32,
}
#endregion
#region Properties
public string? Name { get; set; }
public string? Street { get; set; }
public string? PostalCode { get; set; }
public string? City { get; set; }
public uint Flags { get; set; }
#endregion
#region public static methods
public static async Task<List<Participant>> LoadAll(IDBManager manager)
{
List<DbEntity> loadResultList = await manager.Load(SetLoadQuery, LoadElems);
List<Participant> result = new();
foreach (Participant p in loadResultList.Cast<Participant>())
result.Add(p);
return result;
}
public static List<DbEntity> LoadElems(IDataReader reader)
{
List<DbEntity> result = new List<DbEntity>();
while (reader.Read())
{
Participant p = new();
p.Id = (uint)reader.GetInt32(0);
if (!reader.IsDBNull(1)) p.Name = reader.GetString(1);
if (!reader.IsDBNull(2)) p.Street = reader.GetString(2);
if (!reader.IsDBNull(3)) p.PostalCode = reader.GetString(3);
if (!reader.IsDBNull(4)) p.City = reader.GetString(4);
if (!reader.IsDBNull(5)) p.Flags = (uint)reader.GetInt32(5);
if (!reader.IsDBNull(6)) p.Created = reader.GetDateTime(6);
if (!reader.IsDBNull(7)) p.Modified = reader.GetDateTime(7);
result.Add(p);
}
return result;
}
public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
{
cmd.CommandText = "SELECT id, name, street, postal_code, city, flags, created, modified FROM participant";
}
#endregion
#region abstract method implementation
public override void SetUpdate(IDbCommand cmd)
{
cmd.CommandText = "UPDATE participant set name = @NAME, street = @STREET, postal_code = @POSTAL_CODE, city = @CITY, flags = @FLAGS WHERE id = @ID";
this.SetParameters(cmd);
}
public override void SetCreate(IDbCommand cmd)
{
cmd.CommandText = "INSERT INTO participant (name, street, postal_code, city, flags) VALUES ( @NAME, @STREET, @POSTAL_CODE, @CITY, @FLAGS)";
this.SetParameters(cmd);
}
public override void SetDelete(IDbCommand cmd)
{
cmd.CommandText = "DELETE FROM participant WHERE id = @ID";
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
#endregion
#region private methods
private void SetParameters(IDbCommand cmd)
{
IDbDataParameter name = cmd.CreateParameter();
name.ParameterName = "NAME";
name.Value = this.Name;
cmd.Parameters.Add(name);
IDbDataParameter street = cmd.CreateParameter();
street.ParameterName = "STREET";
street.Value = this.Street;
cmd.Parameters.Add(street);
IDataParameter postal_code = cmd.CreateParameter();
postal_code.ParameterName = "POSTAL_CODE";
postal_code.Value = this.PostalCode;
cmd.Parameters.Add(postal_code);
IDataParameter city = cmd.CreateParameter();
city.ParameterName = "CITY";
city.Value = this.City;
cmd.Parameters.Add(city);
IDataParameter flags = cmd.CreateParameter();
flags.ParameterName = "FLAGS";
flags.Value = this.Flags;
cmd.Parameters.Add(flags);
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
#endregion
#region overrides
public override string ToString()
{
return this.Name ?? $"{base.Id} - {this.GetType().Name}";
}
#endregion
#region public type flag funcs
public bool IsFlagSet(ParticipantType flag)
{
return (this.Flags & (uint)flag) != 0;
}
public void SetFlag(bool value, ParticipantType flag)
{
if (value) this.Flags |= (uint)flag;
else this.Flags &= (uint)~flag;
}
#endregion
}
}

109
src/brecal.model/Role.cs Normal file
View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace brecal.model
{
public class Role : DbEntity
{
#region Properties
public string? Name { get; set; }
public string? Description { get; set; }
#endregion
#region public static methods
public static async Task<List<Role>> LoadAll(IDBManager manager)
{
List<DbEntity> loadResultList = await manager.Load(SetLoadQuery, LoadElems);
List<Role> result = new();
foreach (Role r in loadResultList.Cast<Role>())
result.Add(r);
return result;
}
public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
{
cmd.CommandText = "SELECT id, name, description, created, modified FROM role";
}
public static List<DbEntity> LoadElems(IDataReader reader)
{
List<DbEntity> result = new List<DbEntity>();
while (reader.Read())
{
Role r = new();
r.Id = (uint)reader.GetInt32(0);
if (!reader.IsDBNull(1)) r.Name = reader.GetString(1);
if (!reader.IsDBNull(2)) r.Description = reader.GetString(2);
if (!reader.IsDBNull(3)) r.Created = reader.GetDateTime(3);
if (!reader.IsDBNull(4)) r.Modified = reader.GetDateTime(4);
result.Add(r);
}
return result;
}
#endregion
#region overrides
public override void SetUpdate(IDbCommand cmd)
{
cmd.CommandText = "UPDATE role set name = @NAME, description = @DESC WHERE id = @ID";
this.SetParameters(cmd);
}
public override void SetCreate(IDbCommand cmd)
{
cmd.CommandText = "INSERT INTO role (name, description) VALUES ( @NAME, @DESC)";
this.SetParameters(cmd);
}
public override void SetDelete(IDbCommand cmd)
{
cmd.CommandText = "DELETE FROM role WHERE id = @ID";
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
public override string ToString()
{
return this.Name ?? $"{base.Id} - {this.GetType().Name}";
}
#endregion
#region private methods
private void SetParameters(IDbCommand cmd)
{
IDbDataParameter name = cmd.CreateParameter();
name.ParameterName = "NAME";
name.Value = this.Name;
cmd.Parameters.Add(name);
IDbDataParameter desc = cmd.CreateParameter();
desc.ParameterName = "DESC";
desc.Value = this.Description;
cmd.Parameters.Add(desc);
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
#endregion
}
}

View File

@ -0,0 +1,110 @@
using System.Data;
namespace brecal.model
{
public class RoleAssignment : DbEntity
{
#region Properties
public int? UserId { get; set; }
public int? RoleId { get; set; }
public Role? AssignedRole { get; set; }
public User? AssignedUser { get; set; }
#endregion
#region public static methods
public static async Task<List<RoleAssignment>> LoadForUser(User? u, IDBManager manager)
{
List<DbEntity> loadResultList = await manager.Load(SetLoadQuery, LoadElems, args: u);
List<RoleAssignment> result = new();
foreach (RoleAssignment ra in loadResultList.Cast<RoleAssignment>())
{
ra.AssignedUser = u;
result.Add(ra);
}
return result;
}
public static void SetLoadQuery(IDbCommand cmd, params object?[] args)
{
cmd.CommandText = "SELECT id, user_id, role_id FROM user_role_map WHERE user_id = @UID";
if (args.Length != 1 || !(args[0] is User))
throw new ArgumentException("loader needs single user as argument");
IDataParameter uid = cmd.CreateParameter();
uid.ParameterName = "UID";
if (args[0] is User u)
uid.Value = u.Id;
cmd.Parameters.Add(uid);
}
public static List<DbEntity> LoadElems(IDataReader reader)
{
List<DbEntity> result = new List<DbEntity>();
while (reader.Read())
{
RoleAssignment ra = new();
ra.Id = (uint)reader.GetInt32(0);
if (!reader.IsDBNull(1)) ra.UserId = reader.GetInt32(1);
if (!reader.IsDBNull(2)) ra.RoleId = reader.GetInt32(2);
result.Add(ra);
}
return result;
}
#endregion
#region overrides
public override void SetUpdate(IDbCommand cmd)
{
throw new NotImplementedException();
}
public override void SetCreate(IDbCommand cmd)
{
cmd.CommandText = "INSERT INTO user_role_map (user_id, role_id) VALUES (@USERID, @ROLEID)";
IDbDataParameter userid = cmd.CreateParameter();
userid.ParameterName = "USERID";
userid.Value = this.UserId;
cmd.Parameters.Add(userid);
IDbDataParameter roleid = cmd.CreateParameter();
roleid.ParameterName = "ROLEID";
roleid.Value = this.RoleId;
cmd.Parameters.Add(roleid);
}
public override void SetDelete(IDbCommand cmd)
{
cmd.CommandText = "DELETE FROM user_role_map WHERE id = @ID";
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
public override string ToString()
{
if (this.AssignedRole == null)
{
return $"{Id}: <defunct role>";
}
else
{
return AssignedRole.Name ?? AssignedRole.Id.ToString();
}
}
#endregion
}
}

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace brecal.model
{
public class Securable : DbEntity
{
#region Properties
public string? Name { get; set; }
#endregion
#region public static methods
public static async Task<List<Securable>> LoadAll(IDBManager manager)
{
List<DbEntity> loadResultList = await manager.Load(SetLoadQuery, LoadElems);
List<Securable> result = new();
foreach (Securable s in loadResultList.Cast<Securable>())
result.Add(s);
return result;
}
public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
{
cmd.CommandText = "SELECT id, name, created, modified FROM securable";
}
public static List<DbEntity> LoadElems(IDataReader reader)
{
List<DbEntity> result = new List<DbEntity>();
while (reader.Read())
{
Securable s = new();
s.Id = (uint)reader.GetInt32(0);
if (!reader.IsDBNull(1)) s.Name = reader.GetString(1);
if (!reader.IsDBNull(2)) s.Created = reader.GetDateTime(2);
if (!reader.IsDBNull(3)) s.Modified = reader.GetDateTime(3);
result.Add(s);
}
return result;
}
#endregion
#region overrides
public override void SetUpdate(IDbCommand cmd)
{
cmd.CommandText = "UPDATE securable set name = @NAME WHERE id = @ID";
this.SetParameters(cmd);
}
public override void SetCreate(IDbCommand cmd)
{
cmd.CommandText = "INSERT INTO securable (name) VALUES ( @NAME)";
this.SetParameters(cmd);
}
public override void SetDelete(IDbCommand cmd)
{
cmd.CommandText = "DELETE FROM securable WHERE id = @ID";
this.SetParameters(cmd);
}
public override string ToString()
{
return this.Name ?? $"{base.Id} - {this.GetType().Name}";
}
#endregion
#region private methods
private void SetParameters(IDbCommand cmd)
{
IDbDataParameter name = cmd.CreateParameter();
name.ParameterName = "NAME";
name.Value = this.Name;
cmd.Parameters.Add(name);
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
#endregion
}
}

View File

@ -0,0 +1,109 @@
using System.Data;
namespace brecal.model
{
public class SecurableAssignment : DbEntity
{
#region Properties
public Role? AssignedRole { get; set; }
public Securable? AssignedSecurable { get; set; }
public int? RoleId { get; set; }
public int? SecurableId { get; set; }
#endregion
#region public static methods
public static async Task<List<SecurableAssignment>> LoadForRole(Role? r, IDBManager manager)
{
List<DbEntity> loadResultList = await manager.Load(SetLoadQuery, LoadElems, args: r);
List<SecurableAssignment> result = new();
foreach (SecurableAssignment sa in loadResultList.Cast<SecurableAssignment>())
{
sa.AssignedRole = r;
result.Add(sa);
}
return result;
}
public static void SetLoadQuery(IDbCommand cmd, params object?[] args)
{
cmd.CommandText = "SELECT id, role_id, securable_id FROM role_securable_map WHERE role_id = @RID";
if (args.Length != 1 || !(args[0] is Role))
throw new ArgumentException("loader needs single role as argument");
IDataParameter rid = cmd.CreateParameter();
rid.ParameterName = "RID";
if (args[0] is Role r)
rid.Value = r.Id;
cmd.Parameters.Add(rid);
}
public static List<DbEntity> LoadElems(IDataReader reader)
{
List<DbEntity> result = new List<DbEntity>();
while (reader.Read())
{
SecurableAssignment sa = new();
sa.Id = (uint)reader.GetInt32(0);
if (!reader.IsDBNull(1)) sa.RoleId = reader.GetInt32(1);
if (!reader.IsDBNull(2)) sa.SecurableId = reader.GetInt32(2);
result.Add(sa);
}
return result;
}
#endregion
#region public overrides
public override void SetUpdate(IDbCommand cmd)
{
throw new NotImplementedException();
}
public override void SetCreate(IDbCommand cmd)
{
cmd.CommandText = "INSERT INTO role_securable_map (securable_id, role_id) VALUES (@SECURABLEID, @ROLEID)";
IDbDataParameter userid = cmd.CreateParameter();
userid.ParameterName = "SECURABLEID";
userid.Value = this.SecurableId;
cmd.Parameters.Add(userid);
IDbDataParameter roleid = cmd.CreateParameter();
roleid.ParameterName = "ROLEID";
roleid.Value = this.RoleId;
cmd.Parameters.Add(roleid);
}
public override void SetDelete(IDbCommand cmd)
{
cmd.CommandText = "DELETE FROM role_securable_map WHERE id = @ID";
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
public override string ToString()
{
if (this.AssignedSecurable == null)
{
return $"{Id}: <defunct securable>";
}
else
{
return AssignedSecurable.Name ?? AssignedSecurable.Id.ToString();
}
}
#endregion
}
}

145
src/brecal.model/Ship.cs Normal file
View File

@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace brecal.model
{
public class Ship : DbEntity
{
#region Properties
public string? Name { get; set; }
public int? IMO { get; set; }
public string? Callsign { get; set; }
public uint? Participant_Id { get; set; }
public Participant? Participant { get; set; }
public double? Length { get; set; }
public double? Width { get; set; }
public bool IsTug { get { return Participant_Id != null; } }
public string? TugCompany
{
get { if (Participant != null) return Participant.Name; else return ""; }
}
#endregion
#region public static methods
public static async Task<List<Ship>> LoadAll(IDBManager manager)
{
List<DbEntity> loadResultList = await manager.Load(SetLoadQuery, LoadElems);
List<Ship> result = new();
foreach (Ship s in loadResultList.Cast<Ship>())
result.Add(s);
return result;
}
public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
{
cmd.CommandText = "SELECT id, name, imo, callsign, participant_id, length, width, created, modified FROM ship";
}
public static List<DbEntity> LoadElems(IDataReader reader)
{
List<DbEntity> result = new List<DbEntity>();
while (reader.Read())
{
Ship s = new();
s.Id = (uint)reader.GetInt32(0);
if (!reader.IsDBNull(1)) s.Name = reader.GetString(1);
if (!reader.IsDBNull(2)) s.IMO = reader.GetInt32(2);
if (!reader.IsDBNull(3)) s.Callsign = reader.GetString(3);
if (!reader.IsDBNull(4)) s.Participant_Id = (uint)reader.GetInt32(4);
if (!reader.IsDBNull(5)) s.Length = reader.GetFloat(5);
if (!reader.IsDBNull(6)) s.Width = reader.GetFloat(6);
if (!reader.IsDBNull(7)) s.Created = reader.GetDateTime(7);
if (!reader.IsDBNull(8)) s.Modified = reader.GetDateTime(8);
result.Add(s);
}
return result;
}
#endregion
#region DbEntity implementation
public override void SetCreate(IDbCommand cmd)
{
cmd.CommandText = "INSERT INTO ship (name, imo, callsign, participant_id, length, width) VALUES ( @NAME, @IMO, @CALLSIGN, @PID, @LENGTH, @WIDTH)";
this.SetParameters(cmd);
}
public override void SetDelete(IDbCommand cmd)
{
cmd.CommandText = "DELETE FROM ship WHERE id = @ID";
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
public override void SetUpdate(IDbCommand cmd)
{
cmd.CommandText = "UPDATE ship SET name = @NAME, imo = @IMO, callsign = @CALLSIGN, participant_id = @PID, length = @LENGTH, width = @WIDTH WHERE id = @ID";
this.SetParameters(cmd);
}
#endregion
#region private methods
private void SetParameters(IDbCommand cmd)
{
IDbDataParameter id = cmd.CreateParameter();
id.ParameterName = "ID";
id.Value = this.Id;
cmd.Parameters.Add(id);
IDbDataParameter pid = cmd.CreateParameter();
pid.ParameterName = "PID";
pid.Value = this.Participant_Id;
cmd.Parameters.Add(pid);
IDbDataParameter name = cmd.CreateParameter();
name.ParameterName = "NAME";
name.Value = this.Name;
cmd.Parameters.Add(name);
IDbDataParameter imoparam = cmd.CreateParameter();
imoparam.ParameterName = "IMO";
imoparam.Value = this.IMO;
cmd.Parameters.Add(imoparam);
IDataParameter callsign = cmd.CreateParameter();
callsign.ParameterName = "CALLSIGN";
callsign.Value = this.Callsign;
cmd.Parameters.Add(callsign);
IDataParameter length = cmd.CreateParameter();
length.ParameterName = "LENGTH";
length.Value = this.Length;
cmd.Parameters.Add(length);
IDataParameter width = cmd.CreateParameter();
width.ParameterName = "WIDTH";
width.Value = this.Width;
cmd.Parameters.Add(width);
}
#endregion
}
}

143
src/brecal.model/User.cs Normal file
View File

@ -0,0 +1,143 @@
using System.Data;
namespace brecal.model
{
public class User : DbEntity
{
#region Properties
public string? Firstname { get; set; }
public string? Lastname { get; set; }
public string Username { get; set; } = "";
public string? PasswordHash { get; set; }
public string? APIKey { get; set; }
public uint? Participant_Id { get; set; }
#endregion
#region public static methods
public static async Task<List<User>> LoadForParticipant(Participant? p, IDBManager manager)
{
List<DbEntity> loadResultList = await manager.Load(SetLoadQuery, LoadElems, args: p);
List<User> result = new();
foreach (User u in loadResultList.Cast<User>())
result.Add(u);
return result;
}
public static void SetLoadQuery(IDbCommand cmd, params object?[] args)
{
cmd.CommandText = "SELECT id, first_name, last_name, user_name, api_key, created, modified FROM user WHERE participant_id = @PID";
if (args.Length != 1 || !(args[0] is Participant))
throw new ArgumentException("loader needs single partipant as argument");
IDataParameter pid = cmd.CreateParameter();
pid.ParameterName = "PID";
if (args[0] is Participant p)
pid.Value = p.Id;
cmd.Parameters.Add(pid);
}
public static List<DbEntity> LoadElems(IDataReader reader)
{
List<DbEntity> result = new List<DbEntity>();
while (reader.Read())
{
User u = new();
u.Id = (uint)reader.GetInt32(0);
if (!reader.IsDBNull(1)) u.Firstname = reader.GetString(1);
if (!reader.IsDBNull(2)) u.Lastname = reader.GetString(2);
if (!reader.IsDBNull(3)) u.Username = reader.GetString(3);
if (!reader.IsDBNull(4)) u.APIKey = reader.GetString(4);
if (!reader.IsDBNull(5)) u.Created = reader.GetDateTime(5);
if (!reader.IsDBNull(6)) u.Modified = reader.GetDateTime(6);
result.Add(u);
}
return result;
}
#endregion
#region overrides
public override void SetUpdate(IDbCommand cmd)
{
if(!string.IsNullOrEmpty(this.PasswordHash))
cmd.CommandText = "UPDATE user SET first_name = @FIRSTNAME, last_name = @LASTNAME, user_name = @USERNAME, password_hash = @PWHASH, api_key = @APIKEY WHERE id = @ID";
else
cmd.CommandText = "UPDATE user SET first_name = @FIRSTNAME, last_name = @LASTNAME, user_name = @USERNAME, api_key = @APIKEY WHERE id = @ID";
this.SetParameters(cmd);
}
public override void SetCreate(IDbCommand cmd)
{
cmd.CommandText = "INSERT INTO user (participant_id, first_name, last_name, user_name, password_hash, api_key) VALUES ( @PID, @FIRSTNAME, @LASTNAME, @USERNAME, @PWHASH, @APIKEY)";
this.SetParameters(cmd);
}
public override void SetDelete(IDbCommand cmd)
{
cmd.CommandText = "DELETE FROM user WHERE id = @ID";
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
public override string ToString()
{
return this.Username ?? $"{base.Id} - {this.GetType().Name}";
}
#endregion
#region private methods
private void SetParameters(IDbCommand cmd)
{
IDbDataParameter id = cmd.CreateParameter();
id.ParameterName = "ID";
id.Value = this.Id;
cmd.Parameters.Add(id);
IDbDataParameter pid = cmd.CreateParameter();
pid.ParameterName = "PID";
pid.Value = this.Participant_Id;
cmd.Parameters.Add(pid);
IDbDataParameter firstname = cmd.CreateParameter();
firstname.ParameterName = "FIRSTNAME";
firstname.Value = this.Firstname;
cmd.Parameters.Add(firstname);
IDbDataParameter lastname = cmd.CreateParameter();
lastname.ParameterName = "LASTNAME";
lastname.Value = this.Lastname;
cmd.Parameters.Add(lastname);
IDbDataParameter username = cmd.CreateParameter();
username.ParameterName = "USERNAME";
username.Value = this.Username;
cmd.Parameters.Add(username);
IDbDataParameter pwhash = cmd.CreateParameter();
pwhash.ParameterName = "PWHASH";
pwhash.Value = this.PasswordHash;
cmd.Parameters.Add(pwhash);
IDbDataParameter apikey = cmd.CreateParameter();
apikey.ParameterName = "APIKEY";
apikey.Value = this.APIKey;
cmd.Parameters.Add(apikey);
}
#endregion
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,54 @@
using brecal.model;
using MySqlConnector;
using System.Data;
using System.Runtime.CompilerServices;
using static brecal.model.IDBManager;
namespace brecal.mysql
{
public class DBManager : IDBManager
{
// TODO: remove this and use external configuration
private static readonly string _connectionString = "Server=lager;User ID=ds;Password=HalloWach23;Database=bremen_calling";
public async Task<List<DbEntity>> Load(QueryFunc prepareAction, LoadFunc<IDataReader> loadAction, params object?[] args)
{
await using MySqlConnection connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
using MySqlCommand cmd = new();
cmd.Connection = connection;
prepareAction(cmd, args);
MySqlDataReader reader = await cmd.ExecuteReaderAsync();
List<DbEntity> result = loadAction(reader);
reader.Close();
return result;
}
public async Task<object?> ExecuteScalar(Action<IDbCommand> prepareAction)
{
await using MySqlConnection connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
using MySqlCommand cmd = new();
cmd.Connection = connection;
prepareAction(cmd);
object? result = await cmd.ExecuteScalarAsync();
return result;
}
public async Task<int> ExecuteNonQuery(Action<IDbCommand> prepareAction)
{
await using MySqlConnection connection = new(_connectionString);
await connection.OpenAsync();
using MySqlCommand cmd = new();
cmd.Connection = connection;
prepareAction(cmd);
await cmd.ExecuteNonQueryAsync();
int result = (int)cmd.LastInsertedId;
return result;
}
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MySqlConnector" Version="2.3.0-beta.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\brecal.model\brecal.model.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,47 @@
from flask import Flask
import os
import logging
from . import local_db
from .api import shipcalls
from .api import participant
from .api import times
from .api import notifications
from .api import berths
from .api import ships
from .api import login
sessions = dict()
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev'
)
if test_config is None:
app.config.from_pyfile('config.py', silent=True)
else:
app.config.from_mapping(test_config)
try:
print(f'Instance path = {app.instance_path}')
os.makedirs(app.instance_path)
except OSError:
pass
# Add blueprints
app.register_blueprint(shipcalls.bp)
app.register_blueprint(participant.bp)
app.register_blueprint(times.bp)
app.register_blueprint(notifications.bp)
app.register_blueprint(berths.bp)
app.register_blueprint(ships.bp)
app.register_blueprint(login.bp)
logging.basicConfig(filename='brecal.log', level=logging.DEBUG, format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
local_db.initPool()
logging.info('App started')
return app

View File

@ -0,0 +1,18 @@
from flask import Blueprint, request
from webargs.flaskparser import parser
from .. import impl
from ..services.auth_guard import auth_guard
import json
bp = Blueprint('berths', __name__)
@bp.route('/berths', methods=['get'])
@auth_guard() # no restriction by role
def GetBerths():
if 'Authorization' in request.headers:
token = request.headers.get('Authorization')
return impl.berths.GetBerths(token)
else:
return json.dumps("not authenticated"), 403

View File

@ -0,0 +1,16 @@
from flask import Blueprint, request
from flask_jwt_extended import create_access_token
from webargs.flaskparser import parser
from ..schemas import model
from .. import impl
import json
import logging
bp = Blueprint('login', __name__)
@bp.route('/login', methods=['post'])
def Logon():
options = request.get_json(force=True)
return impl.login.GetUser(options)

View File

@ -0,0 +1,21 @@
from flask import Blueprint, request
from .. import impl
from ..services.auth_guard import auth_guard
import logging
import json
bp = Blueprint('notifications', __name__)
@bp.route('/notifications', methods=['get'])
@auth_guard() # no restriction by role
def GetNotifications():
if 'participant_id' in request.args:
options = {}
options["participant_id"] = request.args.get("participant_id")
options["shipcall_id"] = request.args.get("shipcall_id")
return impl.notifications.GetNotifications(options)
else:
logging.warning("attempt to load notifications without participant id")
return json.dumps("missing argument"), 400

View File

@ -0,0 +1,19 @@
from flask import Blueprint, request
from .. import impl
from ..services.auth_guard import auth_guard
import json
bp = Blueprint('participant', __name__)
@bp.route('/participant', methods=['get'])
@auth_guard() # no restriction by role
def GetParticipant():
if 'Authorization' in request.headers:
token = request.headers.get('Authorization')
options = {}
options["user_id"] = request.args.get("user_id")
return impl.participant.GetParticipant(options)
else:
return json.dumps("not authenticated"), 403

View File

@ -0,0 +1,53 @@
from flask import Blueprint, request
from webargs.flaskparser import parser
from marshmallow import Schema, fields
from ..schemas import model
from .. import impl
from ..services.auth_guard import auth_guard
import logging
import json
bp = Blueprint('shipcalls', __name__)
@bp.route('/shipcalls', methods=['get'])
@auth_guard() # no restriction by role
def GetShipcalls():
if 'Authorization' in request.headers:
token = request.headers.get('Authorization')
options = {}
options["participant_id"] = request.args.get("participant_id")
return impl.shipcalls.GetShipcalls(options)
else:
return json.dumps("not authenticated"), 403
@bp.route('/shipcalls', methods=['post'])
@auth_guard() # no restriction by role
def PostShipcalls():
try:
content = request.get_json(force=True)
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("bad format"), 400
return impl.shipcalls.PostShipcalls(loadedModel)
@bp.route('/shipcalls', methods=['put'])
@auth_guard() # no restriction by role
def PutShipcalls():
try:
content = request.get_json(force=True)
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("bad format"), 400
return impl.shipcalls.PutShipcalls(loadedModel)

View File

@ -0,0 +1,16 @@
from flask import Blueprint, request
from .. import impl
from ..services.auth_guard import auth_guard
import json
bp = Blueprint('ships', __name__)
@bp.route('/ships', methods=['get'])
@auth_guard() # no restriction by role
def GetShips():
if 'Authorization' in request.headers:
token = request.headers.get('Authorization')
return impl.ships.GetShips(token)
else:
return json.dumps("not authenticated"), 403

View File

@ -0,0 +1,69 @@
from flask import Blueprint, request
from ..schemas import model
from .. import impl
from ..services.auth_guard import auth_guard
import json
import logging
bp = Blueprint('times', __name__)
@bp.route('/times', methods=['get'])
@auth_guard() # no restriction by role
def GetTimes():
options = {}
options["shipcall_id"] = request.args.get("shipcall_id")
return impl.times.GetTimes(options)
@bp.route('/times', methods=['post'])
@auth_guard() # no restriction by role
def PostTimes():
try:
# print (request.is_json)
content = request.get_json(force=True) # force gets us json even if the content-type was wrong
# print (content)
# body = parser.parse(schema, request, location='json')
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("bad format"), 400
return impl.times.PostTimes(loadedModel)
@bp.route('/times', methods=['put'])
@auth_guard() # no restriction by role
def PutTimes():
try:
content = request.get_json(force=True)
loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("bad format"), 400
return impl.times.PutTimes(loadedModel)
@bp.route('/times', methods=['delete'])
@auth_guard() # no restriction by role
def DeleteTimes():
# TODO check if I am allowd to delete this thing by deriving the participant from the bearer token
if 'id' in request.args:
options = {}
options["id"] = request.args.get("id")
return impl.times.DeleteTimes(options)
else:
logging.warning("Times delete missing id argument")
return json.dumps("missing argument"), 400

View File

@ -0,0 +1,10 @@
{
"host" : "lager",
"port" : 3306,
"user" : "ds",
"password" : "HalloWach23",
"pool_name" : "brecal_pool",
"pool_size" : 20,
"database" : "bremen_calling",
"autocommit" : true
}

View File

@ -0,0 +1,7 @@
from . import berths
from . import notifications
from . import participant
from . import shipcalls
from . import times
from . import ships
from . import login

View File

@ -0,0 +1,29 @@
import json
import logging
import pydapper
from ..schemas import model
from .. import local_db
def GetBerths(token):
"""
No parameters, gets all entries
"""
# TODO: validate token
try:
commands = pydapper.using(local_db.connection_pool)
data = commands.query("SELECT id, name, participant_id, `lock`, created, modified FROM berth ORDER BY name", model=model.Berth)
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
return json.dumps(data, default=model.obj_dict), 200

View File

@ -0,0 +1,46 @@
import json
import logging
import pydapper
import bcrypt
from ..schemas import model
from .. import local_db
from ..services import jwt_handler
def GetUser(options):
try:
if "password" in options and "username" in options:
hash = bcrypt.hashpw(options["password"].encode('utf-8'), bcrypt.gensalt( 12 )).decode('utf8')
commands = pydapper.using(local_db.connection_pool)
data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, api_key FROM user WHERE user_name = ?username? OR user_email = ?username?",
model=model.User, param={"username" : options["username"]})
# print(data)
if len(data) == 1:
if bcrypt.checkpw(options["password"].encode("utf-8"), bytes(data[0].password_hash, "utf-8")):
result = {
"id": data[0].id,
"participant_id": data[0].participant_id,
"first_name": data[0].first_name,
"last_name": data[0].last_name,
"user_name": data[0].user_name,
"user_phone": data[0].user_phone
}
token = jwt_handler.generate_jwt(payload=result, lifetime=60) # generate token valid 60 mins
result["token"] = token # add token to user data
return json.dumps(result), 200
if len(data) > 1:
return json.dumps("credential lookup mismatch"), 500
return json.dumps("invalid credentials"), 403
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
# $2b$12$uWLE0r32IrtCV30WkMbVwOdltgeibymZyYAf4ZnQb2Bip8hrkGGwG
# $2b$12$.vEapj9xU8z0RK0IpIGeYuRIl0ktdMt4XdJQBhVn.3K2hmvm7qD3y
# $2b$12$yL3PiseU70ciwEuMVM4OtuMwR6tNuIT9vvBiBG/uyMrPxa16E2Zqu

View File

@ -0,0 +1,29 @@
import json
import logging
import pydapper
from ..schemas import model
from .. import local_db
def GetNotifications(options):
"""
:param options: A dictionary containing all the paramters for the Operations
options["participant_id"]: **Id of participant**. *Example: 2*. Id returned through loading of participant
options["shipcall_id"]: **Id of ship call**. *Example: 52*. Id given in ship call list
"""
# Implement your business logic here
# All the parameters are present in the options argument
return json.dumps({
"acknowledged": "<boolean>",
"id": "<integer>",
"notification_type": "<string>",
"participant_id": "<integer>",
"times_id": "<integer>",
"timestamp": "<date-time>",
}), 200

View File

@ -0,0 +1,29 @@
import json
import logging
import pydapper
from ..schemas import model
from .. import local_db
def GetParticipant(options):
"""
:param options: A dictionary containing all the paramters for the Operations
options["user_id"]: **Id of user**. *Example: 2*. User id returned by login call.
"""
# TODO: validate token
try:
commands = pydapper.using(local_db.connection_pool)
if "user_id" in options and options["user_id"]:
data = commands.query("SELECT p.id as id, p.name as name, p.street as street, p.postal_code as postal_code, p.city as city, p.flags as flags, p.created as created, p.modified as modified FROM p INNER JOIN user u WHERE u.participant_id = p.id and u.id = ?userid?", model=model.Participant, param={"userid" : options["user_id"]})
else:
data = commands.query("SELECT p.id as id, p.name as name, p.street as street, p.postal_code as postal_code, p.city as city, p.flags as flags, p.created as created, p.modified as modified FROM participant p ORDER BY p.name", model=model.Participant)
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
return json.dumps(data, default=model.obj_dict), 200

View File

@ -0,0 +1,110 @@
import json
import logging
import pydapper
from ..schemas import model
from .. import local_db
def GetShipcalls(options):
"""
No parameters, gets all entries
"""
# TODO: validate token
try:
commands = pydapper.using(local_db.connection_pool)
data = commands.query("SELECT id, ship_id, type, eta, voyage, etd, arrival_berth_id, departure_berth_id, tug_required, pilot_required, " +
"flags, pier_side, bunkering, replenishing, draft, tidal_window_from, tidal_window_to, rain_sensitive_cargo, recommended_tugs, " +
"created, modified FROM shipcall WHERE eta IS NULL OR eta >= DATE(NOW() - INTERVAL 2 DAY) " +
"ORDER BY eta", model=model.Shipcall)
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
return json.dumps(data, default=model.obj_dict), 200
def PostShipcalls(schemaModel):
"""
:param schemaModel: The deserialized dict of the request
"""
# TODO: Validate the upload data
# This creates a *new* entry
try:
commands = pydapper.using(local_db.connection_pool)
query = "INSERT INTO shipcall ("
isNotFirst = False
for key in schemaModel.keys():
if key == "id":
continue
if isNotFirst:
query += ","
isNotFirst = True
query += key
query += ") VALUES ("
isNotFirst = False
for key in schemaModel.keys():
if key == "id":
continue
if isNotFirst:
query += ","
isNotFirst = True
query += "?" + key + "?"
query += ")"
commands.execute(query, schemaModel)
new_id = commands.execute_scalar("select last_insert_id()")
return json.dumps({"id" : new_id}), 201
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
def PutShipcalls(schemaModel):
"""
:param schemaModel: The deserialized dict of the request
"""
# This updates an *existing* entry
try:
commands = pydapper.using(local_db.connection_pool)
query = "UPDATE shipcall SET "
isNotFirst = False
for key in schemaModel.keys():
if key == "id":
continue
if isNotFirst:
query += ", "
isNotFirst = True
query += key + " = ?" + key + "? "
query += "WHERE id = ?id?"
affected_rows = commands.execute(query, param=schemaModel)
if affected_rows == 1:
return json.dumps({"id" : schemaModel["id"]}), 200
return json.dumps("no such record"), 404
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500

View File

@ -0,0 +1,29 @@
import json
import logging
import pydapper
from ..schemas import model
from .. import local_db
def GetShips(token):
"""
No parameters, gets all entries
"""
# TODO: validate token
try:
commands = pydapper.using(local_db.connection_pool)
data = commands.query("SELECT id, name, imo, callsign, participant_id, length, width, created, modified FROM ship ORDER BY name", model=model.Ship)
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
return json.dumps(data, default=model.obj_dict), 200

View File

@ -0,0 +1,126 @@
import json
import logging
import pydapper
from ..schemas import model
from .. import local_db
def GetTimes(options):
"""
:param options: A dictionary containing all the paramters for the Operations
options["shipcall_id"]: **Id**. *Example: 42*. Id of referenced ship call.
"""
# TODO: validate token
try:
commands = pydapper.using(local_db.connection_pool)
data = commands.query("SELECT id, start_planned, end_planned, duration_planned, start_actual, end_actual, duration_actual, shipcall_id, participant_id, created, modified FROM times " +
"WHERE times.shipcall_id = ?scid?", model=model.Times, param={"scid" : options["shipcall_id"]})
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
return json.dumps(data, default=model.obj_dict), 200
def PostTimes(schemaModel):
"""
:param schemaModel: The deserialized model of the record to be inserted
"""
# TODO: Validate the upload data
# This creates a *new* entry
try:
commands = pydapper.using(local_db.connection_pool)
query = "INSERT INTO times ("
isNotFirst = False
for key in schemaModel.keys():
if key == "id":
continue
if isNotFirst:
query += ","
isNotFirst = True
query += key
query += ") VALUES ("
isNotFirst = False
for key in schemaModel.keys():
if key == "id":
continue
if isNotFirst:
query += ","
isNotFirst = True
query += "?" + key + "?"
query += ")"
commands.execute(query, schemaModel)
new_id = commands.execute_scalar("select last_insert_id()")
return json.dumps({"id" : new_id}), 201
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
def PutTimes(schemaModel):
"""
:param schemaModel: The deserialized model of the record to be inserted
"""
# This updates an *existing* entry
try:
commands = pydapper.using(local_db.connection_pool)
query = "UPDATE times SET "
isNotFirst = False
for key in schemaModel.keys():
if key == "id":
continue
if isNotFirst:
query += ", "
isNotFirst = True
query += key + " = ?" + key + "? "
query += "WHERE id = ?id?"
affected_rows = commands.execute(query, param=schemaModel)
if affected_rows == 1:
return json.dumps({"id" : schemaModel["id"]}), 200
return json.dumps("no such record"), 404
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500
def DeleteTimes(options):
"""
:param options: A dictionary containing all the paramters for the Operations
options["id"]
"""
try:
commands = pydapper.using(local_db.connection_pool)
affected_rows = commands.execute("DELETE FROM times WHERE id = ?id?", param={"id" : options["id"]})
if affected_rows == 1:
return json.dumps({"id" : options["id"]}), 200
return json.dumps("no such record"), 404
except Exception as ex:
logging.error(ex)
print(ex)
return json.dumps("call failed"), 500

View File

@ -0,0 +1,32 @@
import mysql.connector
import pydapper
import logging
import json
import os
connection_pool = None
def initPool():
try:
global connection_pool
config_path = './src/server/BreCal/connection_data.json'
print (os.getcwd())
if not os.path.exists(config_path):
print ('cannot find ' + config_path)
exit(1)
f = open(config_path);
connection_data = json.load(f)
connection_pool = mysql.connector.connect(**connection_data)
commands = pydapper.using(connection_pool)
data = commands.query("SELECT id from `user`")
print("DB connection successful")
except mysql.connector.PoolError as e:
logging.error(f"Failed to create connection pool: {e}")
print(e)
except Exception as e:
print(e)

View File

@ -0,0 +1,187 @@
from marshmallow import Schema, fields, INCLUDE, ValidationError
from marshmallow_dataclass import dataclass
import json
import datetime
def obj_dict(obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
return obj.__dict__
@dataclass
class Berth(Schema):
id: int
name: str
participant_id: int
lock: bool
created: datetime
modified: datetime
class Error(Schema):
message = fields.String(required=True,)
class GetVerifyInlineResp(Schema):
pass
@dataclass
class Notification(Schema):
id: int
times_id: int
acknowledged: bool
level: int
type: int
message: str
created: datetime
modified: datetime
@dataclass
class Participant(Schema):
id: int
name: str
street: str
postal_code: str
city: str
flags: int
created: datetime
modified: datetime
class ParticipantList(Participant):
pass
class ShipcallSchema(Schema):
def __init__(self):
super().__init__(unknown=None)
pass
id = fields.Int()
ship_id = fields.Int()
type = fields.Int()
eta = fields.DateTime()
voyage = fields.Str()
etd = fields.DateTime()
arrival_berth_id = fields.Int()
departure_berth_id = fields.Int()
tug_required = fields.Bool()
pilot_required = fields.Bool()
flags = fields.Int()
pier_side = fields.Bool()
bunkering = fields.Bool()
replenishing = fields.Bool()
draft = fields.Float()
tidal_window_from = fields.DateTime()
tidal_window_to = fields.DateTime()
rain_sensitive_cargo = fields.Bool()
recommended_tugs = fields.Int()
created = fields.DateTime()
modified = fields.DateTime()
@dataclass
class Shipcall:
id: int
ship_id: int
type: int
eta: datetime
voyage: str
etd: datetime
arrival_berth_id: int
departure_berth_id: int
tug_required: bool
pilot_required: bool
flags: int
pier_side: bool
bunkering: bool
replenishing: bool
draft: float
tidal_window_from: datetime
tidal_window_to: datetime
rain_sensitive_cargo: bool
recommended_tugs: int
created: datetime
modified: datetime
class ShipcallId(Schema):
pass
# this is the way!
class TimesSchema(Schema):
def __init__(self):
super().__init__(unknown=None)
pass
id = fields.Int(Required=False)
start_planned = fields.DateTime(Required=False)
end_planned = fields.DateTime(Required = False)
duration_planned = fields.Int(Required = False)
start_actual = fields.DateTime(Required = False)
end_actual = fields.DateTime(Required = False)
duration_actual = fields.Int(Required = False)
participant_id = fields.Int(Required = True)
shipcall_id = fields.Int(Required = True)
created = fields.DateTime(Required = False)
modified = fields.DateTime(Required = False)
@dataclass
class Times:
id: int
start_planned: datetime
end_planned: datetime
duration_planned: int
start_actual: datetime
end_actual: datetime
duration_actual: int
participant_id: int
shipcall_id: int
created: datetime
modified: datetime
@dataclass
class User:
id: int
participant_id: int
first_name: str
last_name: str
user_name: str
user_email: str
user_phone: str
password_hash: str
api_key: str
@dataclass
class Ship(Schema):
id: int
name: str
imo: int
callsign: str
participant_id: int
length: float
width: float
created: datetime
modified: datetime
class TimesId(Schema):
pass
class BerthList(Berth):
pass
class NotificationList(Notification):
pass
class Shipcalls(Shipcall):
pass
class TimesList(Times):
pass

View File

@ -0,0 +1,34 @@
import json
from flask import request
from .jwt_handler import decode_jwt
def check_jwt():
# get header and try to get payload
# this will throw an exception if the payload is missing, invalid or expired
token = request.headers.get('Authorization')
if not token:
raise Exception('Missing access token')
jwt = token.split('Bearer ')[1]
try:
return decode_jwt(jwt)
except Exception as e:
raise Exception(f'invalid access token: {e}')
# magic. use this to decorate the api calls
# https://brunotatsuya.dev/blog/jwt-authentication-and-authorization-for-python-flask-rest-apis
def auth_guard(role=None):
def wrapper(route_function):
def decorated_function(*args, **kwargs):
# Authentication gate
try:
user_data = check_jwt()
except Exception as e:
return json.dumps({"message" : f'{e}', "status": 401}), 401
if role and role not in user_data['roles']:
return json.dumps({"message": 'Authorization required.', "status" : 403}), 403
# get on to original route
return route_function(*args, **kwargs)
decorated_function.__name__ = route_function.__name__
return decorated_function
return wrapper

View File

@ -0,0 +1,17 @@
import os
import jwt
import datetime
import secrets
def create_api_key():
return secrets.token_urlsafe(16)
def generate_jwt(payload, lifetime=None):
if lifetime:
payload['exp'] = (datetime.datetime.now() + datetime.timedelta(minutes=lifetime)).timestamp()
return jwt.encode(payload, os.environ.get('SECRET_KEY'), algorithm="HS256")
def decode_jwt(token):
return jwt.decode(token, os.environ.get('SECRET_KEY'), algorithms=["HS256"])

22
src/server/README.md Normal file
View File

@ -0,0 +1,22 @@
# BreCal
## OpenAPI 3.0 Generated Flask project
The code was originally created by exporting the API from Postman. Selecting the API on the left (root element) the code generation icon becomes visible on the right.
![image](../../docs/export_flask_project.png)
If the API gets updated I think it makes most sense to export again into a API helper branch and then merge this branch back into the project. Please do not forget to update the [index.yaml](../../misc/index.yaml) file.
The branch is named yaml_export.
All the routes are defined in 'project/api' folder.
Each route parses the request and calls the corresponding function in the 'project/impl' directory passing all the parameters and request body as function arguments.
To run this project:
```bash
pip install -r requirements.txt
export FLASK_APP='BreCal'
export FLASK_ENV=development
flask run
```

12
src/server/flaskapp.wsgi Normal file
View File

@ -0,0 +1,12 @@
import sys
import logging
sys.path.insert(0, '/var/www/brecal/server')
sys.path.insert(0, '/var/www/brecal/venv/lib/python3.10/site-packages/')
# Set up logging
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
# Import and run the Flask app
from BreCal import create_app
application = create_app()

View File

@ -0,0 +1,9 @@
click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
marshmallow==3.9.1
webargs==6.1.1
Werkzeug==1.0.1
pydapper[mysql-connector-python]

12
src/server/setup.py Normal file
View File

@ -0,0 +1,12 @@
from setuptools import find_packages, setup
setup(
name='BreCal',
version='1.0.0',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=[
'flask'
]
)