diff --git a/migrations/V001.030__account_key.sql b/migrations/V001.030__account_key.sql new file mode 100644 index 0000000..65e4cd5 --- /dev/null +++ b/migrations/V001.030__account_key.sql @@ -0,0 +1,43 @@ +/* +Template migration. +*/ + +DROP TABLE IF EXISTS account_key_version; +DROP TABLE IF EXISTS account_key; + +CREATE TABLE account_key ( + id BIGINT NOT NULL PRIMARY KEY DEFAULT nextval('id_seq'), + + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_by BIGINT NOT NULL REFERENCES account, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_by BIGINT NOT NULL REFERENCES account, + + account_id BIGINT NOT NULL REFERENCES account, + comment TEXT, + public_key TEXT NOT NULL, + key_type VARCHAR(50) NOT NULL +); +GRANT ALL ON account_key TO "p2k16-web"; + +CREATE TABLE account_key_version +( + transaction_id BIGINT NOT NULL REFERENCES transaction, + end_transaction_id BIGINT REFERENCES transaction, + operation_type INT NOT NULL, + + id BIGINT NOT NULL, + + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_by BIGINT NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_by BIGINT NOT NULL, + + account_id BIGINT, + comment TEXT, + public_key TEXT, + key_type VARCHAR(50) +); +GRANT INSERT, UPDATE ON account_key_version TO "p2k16-web"; +GRANT ALL ON account_key_version TO "p2k16-web"; + diff --git a/web/src/p2k16/core/models.py b/web/src/p2k16/core/models.py index 028f68b..883514a 100644 --- a/web/src/p2k16/core/models.py +++ b/web/src/p2k16/core/models.py @@ -624,6 +624,56 @@ def __init__(self, tool: ToolDescription, account: Account, started: DateTime): def find_by_tool(_tool) -> Optional['ToolCheckout']: return ToolCheckout.query.filter(ToolCheckout.tool_description_id == _tool.id).one_or_none() +# Also specified in the core_blueprint.py +class AccountKeyType(enum.Enum): + SSH = 1 + WIREGUARD = 2 + + +class AccountKey(DefaultMixin, db.Model): + __tablename = 'account_key' + __versioned__ = {} + + account_id = Column("account_id", Integer, ForeignKey('account.id'), nullable=False) + comment = Column("comment", String(200)) + public_key = Column("public_key", String(10000), nullable=False) + _key_type = Column("key_type", String(50), nullable=False) + + account = relationship("Account", foreign_keys=[account_id]) + + def __init__(self, account: Account, comment: str, public_key: str, + key_type: AccountKeyType): + super().__init__() + self.account_id = account.id + self.comment = comment + self.public_key = public_key + self._key_type = key_type.name + + def __repr__(self): + return '' % (self.id, self.account_id, self._key_type, self.comment) + + + @hybrid_property + def key_type(self) -> AccountKeyType: + return AccountKeyType[self._key_type] + + @staticmethod + def find_by_id(_id) -> Optional["AccountKey"]: + return AccountKey.query.filter(AccountKey.id == _id).one_or_none() + + @staticmethod + def get_by_id(_id) -> "AccountKey": + return AccountKey.query.filter(AccountKey.id == _id).one() + + @staticmethod + def delete_by_id(_id): + c = AccountKey.get_by_id(_id) + db.session.delete(c) + db.session.flush() + + @staticmethod + def find_by_account(_account) -> List["AccountKey"]: + return AccountKey.query.filter(AccountKey.account_id == _account.id).all() from sqlalchemy import event diff --git a/web/src/p2k16/web/core_blueprint.py b/web/src/p2k16/web/core_blueprint.py index 9447a42..d0fc049 100644 --- a/web/src/p2k16/web/core_blueprint.py +++ b/web/src/p2k16/web/core_blueprint.py @@ -13,7 +13,9 @@ from p2k16.core.membership_management import member_create_checkout_session, member_customer_portal, \ get_membership, get_membership_payments, active_member, get_membership_fee from p2k16.core.models import Account, Circle, Company, CompanyEmployee, CircleMember, BadgeDescription, \ - CircleManagementStyle, Membership, StripePayment + CircleManagementStyle, Membership, StripePayment, \ + AccountKeyType, AccountKey + from p2k16.core.models import AccountBadge from p2k16.core.models import db from p2k16.web.utils import validate_schema, require_circle_membership, DataServiceTool, ResourcesTool @@ -26,6 +28,8 @@ bool_type = {"type": "boolean"} management_style_type = {"enum": ["ADMIN_CIRCLE", "SELF_ADMIN"]} stripe_pubkey = None +# also specified in models.py +account_key_type = {"enum": ["SSH", "WIREGUARD"]} register_account_form = { "type": "object", @@ -124,6 +128,19 @@ "required": ["name", "contact"] } +add_account_key_form = { + "type": "object", + "properties": { + "comment": nonempty_string, + "publicKey": nonempty_string, + "keyType": account_key_type, + }, + "required": [ + "comment", + "publicKey", + "keyType", + ] +} def model_to_json(obj) -> dict: d = {} @@ -185,6 +202,15 @@ def account_to_json(account: Account): "phone": account.phone, }} +def account_key_to_json(ak: AccountKey): + return {**model_to_json(ak), **{ + "id": ak.id, + "account_id": ak.account.id, + "account_username": ak.account.username, + "comment": ak.comment, + "public_key": ak.public_key, + "key_type": ak.key_type.name, + }} def profile_to_json(account: Account, circles: List[Circle], badges: Optional[List[AccountBadge]], full=False): from .badge_blueprint import badge_to_json @@ -469,6 +495,51 @@ def _manage_circle_membership(create: bool): db.session.commit() return jsonify(circle_to_json(circle, include_members=True)) +############################################################################### +# AccountKey + +def _return_account_key(account): + keys = AccountKey.find_by_account(account) + logger.info("keys: {}" % keys) + ret = [account_key_to_json(ak) for ak in + keys] + + return jsonify(ret) +@registry.route('/data/account/key', methods=["GET"]) +def data_account_keys(): + account = flask_login.current_user.account + + return _return_account_key(account) + +@registry.route('/data/account/key', methods=["POST"]) +@validate_schema(add_account_key_form) +def service_add_account_key(): + account = flask_login.current_user.account + comment = request.json.get('comment') + public_key = request.json.get('publicKey') + key_type = AccountKeyType[request.json.get('keyType')] + + key = AccountKey(account, comment, public_key, key_type) + db.session.add(key) + db.session.commit(); + + return _return_account_key(account) + +@registry.route('/data/account/key/', methods=["DELETE"]) +def service_remove_account_key(key_id): + account = flask_login.current_user.account + key = AccountKey.find_by_id(key_id) + + if key is None: + abort(404) + + if key.account.id != account.id: + raise P2k16UserException("Key not owned by logged in user: {}".format(account.id)) + + db.session.delete(key) + db.session.commit() + return _return_account_key(account) + ############################################################################### # Membership diff --git a/web/src/p2k16/web/static/core-data-service.js b/web/src/p2k16/web/static/core-data-service.js index 9ac584d..f43cf93 100644 --- a/web/src/p2k16/web/static/core-data-service.js +++ b/web/src/p2k16/web/static/core-data-service.js @@ -75,6 +75,30 @@ function CoreDataService($http) { return $http(req); }; + this.data_account_keys = function () { + var req = {}; + req.method = 'GET'; + req.url = '/data/account/key'; + return $http(req); + }; + + this.service_add_account_key = function (payload) { + var req = {}; + req.method = 'POST'; + req.url = '/data/account/key'; + req.data = payload; + return $http(req); + }; + + this.service_remove_account_key = function (key_id, payload) { + var req = {}; + req.method = 'DELETE'; + req.url = '/data/account/key'; + req.url += '/' + key_id; + req.data = payload; + return $http(req); + }; + this.membership_create_checkout_session = function (payload) { var req = {}; req.method = 'POST'; @@ -214,6 +238,9 @@ CoreDataServiceResolvers.data_account_summary = function (CoreDataService, $rout var account_id = $route.current.params.account_id; return CoreDataService.data_account_summary(account_id).then(function (res) { return res.data; }); }; +CoreDataServiceResolvers.data_account_keys = function (CoreDataService) { + return CoreDataService.data_account_keys().then(function (res) { return res.data; }); +}; CoreDataServiceResolvers.data_circle = function (CoreDataService, $route) { var circle_id = $route.current.params.circle_id; return CoreDataService.data_circle(circle_id).then(function (res) { return res.data; }); diff --git a/web/src/p2k16/web/static/my-profile.html b/web/src/p2k16/web/static/my-profile.html index a7698a5..1139ca6 100644 --- a/web/src/p2k16/web/static/my-profile.html +++ b/web/src/p2k16/web/static/my-profile.html @@ -5,21 +5,21 @@

My profile

- +
- +
- +
@@ -30,7 +30,7 @@

My profile

Edit profile details

- +
Edit profile details
-
+
@@ -77,4 +77,63 @@

Box label

+

Account keys

+
+
+

+ + + {{key.comment}} +

+
+
+

Add key

+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ + diff --git a/web/src/p2k16/web/static/p2k16/p2k16.css b/web/src/p2k16/web/static/p2k16/p2k16.css index 836e441..73256e9 100644 --- a/web/src/p2k16/web/static/p2k16/p2k16.css +++ b/web/src/p2k16/web/static/p2k16/p2k16.css @@ -1,5 +1,5 @@ div.error-container { - position: absolute; + position: fixed; right: 3em; top: 3em; width: 25em; diff --git a/web/src/p2k16/web/static/p2k16/p2k16.js b/web/src/p2k16/web/static/p2k16/p2k16.js index b272593..fd0e321 100644 --- a/web/src/p2k16/web/static/p2k16/p2k16.js +++ b/web/src/p2k16/web/static/p2k16/p2k16.js @@ -41,7 +41,8 @@ controllerAs: 'ctrl', templateUrl: p2k16_resources.my_profile_html, resolve: { - badgeDescriptions: BadgeDataServiceResolvers.badge_descriptions + badgeDescriptions: BadgeDataServiceResolvers.badge_descriptions, + accountKeys: CoreDataServiceResolvers.data_account_keys } }).when("/tool", { controller: ToolFrontPageController, @@ -689,7 +690,7 @@ * @param {LabelService} LabelService * @constructor */ - function MyProfileController($scope, P2k16, CoreDataService, badgeDescriptions, LabelService) { + function MyProfileController($scope, P2k16, CoreDataService, badgeDescriptions, LabelService, accountKeys) { var self = this; P2k16.accountListeners.add($scope, function (newValue) { @@ -728,18 +729,39 @@ }); } + function addAccountKey() { + CoreDataService.service_add_account_key(self.newAccountKeyForm).then(function (res) { + var msg = res.message || "Key added to account"; + P2k16.addInfos(msg); + // update client data + self.accountKeys = res.data; + }); + } + + function removeAccountKey(key) { + CoreDataService.service_remove_account_key(key.id).then(function (res) { + var msg = res.message || "Key removed from account"; + P2k16.addInfos(msg); + self.accountKeys = res.data; + }); + } + + self.badges = []; self.circles = []; self.newBadge = {}; self.descriptions = badgeDescriptions; + self.accountKeys = accountKeys; self.changePasswordForm = {}; self.changePassword = changePassword; self.printBoxLabel = printBoxLabel; - + self.addAccountKey = addAccountKey; + self.removeAccountKey = removeAccountKey; self.saveProfile = saveProfile; self.profileForm = { phone: P2k16.currentProfile().account.phone }; + self.newAccountKeyForm = {}; updateBadges(P2k16.currentProfile()); updateCircles(P2k16.currentProfile());