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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions migrations/sql/V2025.10.30.05.04__minespace_user_new_columns.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- Add new columns to minespace_user table to align with user table structure
ALTER TABLE minespace_user
ADD COLUMN sub VARCHAR,
ADD COLUMN email VARCHAR,
ADD COLUMN given_name VARCHAR,
ADD COLUMN family_name VARCHAR,
ADD COLUMN display_name VARCHAR,
ADD COLUMN identity_provider VARCHAR,
ADD COLUMN bceid_user_guid VARCHAR,
ADD COLUMN last_logged_in TIMESTAMPTZ,
ADD COLUMN create_user VARCHAR(255),
ADD COLUMN create_timestamp TIMESTAMP WITH TIME ZONE DEFAULT now(),
ADD COLUMN update_user VARCHAR(255),
ADD COLUMN update_timestamp TIMESTAMP WITH TIME ZONE DEFAULT now();

ALTER TABLE minespace_user
RENAME COLUMN email_or_username TO bceid_username;

ALTER TABLE minespace_user
DROP COLUMN IF EXISTS keycloak_guid;

COMMENT ON COLUMN minespace_user.sub IS 'User subject identifier from identity provider';
COMMENT ON COLUMN minespace_user.email IS 'User email address';
COMMENT ON COLUMN minespace_user.given_name IS 'User given/first name';
COMMENT ON COLUMN minespace_user.family_name IS 'User family/last name';
COMMENT ON COLUMN minespace_user.display_name IS 'User display name';
COMMENT ON COLUMN minespace_user.bceid_username IS 'BCeID username (renamed from email_or_username)';
COMMENT ON COLUMN minespace_user.identity_provider IS 'Identity provider used for authentication';
COMMENT ON COLUMN minespace_user.bceid_user_guid IS 'BCeID user GUID';
COMMENT ON COLUMN minespace_user.last_logged_in IS 'Timestamp of last login';
COMMENT ON COLUMN minespace_user.create_user IS 'User who created the record';
COMMENT ON COLUMN minespace_user.create_timestamp IS 'Timestamp when record was created';
COMMENT ON COLUMN minespace_user.update_user IS 'User who last updated the record';
COMMENT ON COLUMN minespace_user.update_timestamp IS 'Timestamp when record was last updated';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- This file was generated by the generate_history_table_ddl command
-- The file contains the corresponding history table definition for the minespace_user table
CREATE TABLE minespace_user_version (
create_user VARCHAR(60),
create_timestamp TIMESTAMP WITHOUT TIME ZONE,
update_user VARCHAR(60),
update_timestamp TIMESTAMP WITHOUT TIME ZONE,
deleted_ind BOOLEAN default FALSE,
user_id INTEGER NOT NULL,
bceid_username VARCHAR(100),
sub VARCHAR,
email VARCHAR,
given_name VARCHAR,
family_name VARCHAR,
display_name VARCHAR,
identity_provider VARCHAR,
bceid_user_guid VARCHAR,
last_logged_in TIMESTAMP WITHOUT TIME ZONE,
transaction_id BIGINT NOT NULL,
end_transaction_id BIGINT,
operation_type SMALLINT NOT NULL,
PRIMARY KEY (user_id, transaction_id)
);
CREATE INDEX ix_minespace_user_version_operation_type ON minespace_user_version (operation_type);
CREATE INDEX ix_minespace_user_version_end_transaction_id ON minespace_user_version (end_transaction_id);
CREATE INDEX ix_minespace_user_version_transaction_id ON minespace_user_version (transaction_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- This file was generated by the generate_history_table_ddl command
-- The file contains the data migration to backfill history records for the minespace_user table
with transaction AS (insert into transaction(id) values(DEFAULT) RETURNING id)
insert into minespace_user_version (transaction_id, operation_type, end_transaction_id, "create_user", "create_timestamp", "update_user", "update_timestamp", "deleted_ind", "user_id", "bceid_username", "sub", "email", "given_name", "family_name", "display_name", "identity_provider", "bceid_user_guid", "last_logged_in")
select t.id, '0', null, "create_user", "create_timestamp", "update_user", "update_timestamp", "deleted_ind", "user_id", "bceid_username", "sub", "email", "given_name", "family_name", "display_name", "identity_provider", "bceid_user_guid", "last_logged_in"
from "minespace_user",transaction t;
7 changes: 5 additions & 2 deletions services/common/src/components/mine/MineUserAccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { FC, useEffect } from "react";
import { useAppDispatch, useAppSelector } from "@mds/common/redux/rootState";
import { fetchMinespaceUsersByMine, getMinespaceUsersByMineGuid } from "@mds/common/redux/slices/minespaceSlice";
import CoreTable from "../common/CoreTable";
import { renderTextColumn } from "../common/CoreTableCommonColumns";
import { renderDateColumn, renderTextColumn } from "../common/CoreTableCommonColumns";
import { Col, Row, Typography } from "antd";
import { getIsCore } from "@mds/common/redux/reducers/authenticationReducer";
import { MDS_EMAIL } from "@mds/common/constants/strings";
Expand All @@ -24,7 +24,10 @@ const MineUserAccess: FC<MineUserAccessParams> = ({ mineGuid }) => {
}, []);

const columns = [
renderTextColumn("email_or_username", "BCeID/Email", true)
renderTextColumn("display_name", "Name", true),
renderTextColumn("bceid_username", "BCeID", true),
renderTextColumn("email", "Email", true),
renderDateColumn("last_logged_in", "Last Login", true),
];

return (<Row>
Expand Down
8 changes: 8 additions & 0 deletions services/common/src/interfaces/mine.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ export interface IMine {
latest_mine_status: IMineStatus;
mine_status: IMineStatus[];
}

export interface IMineName {
mine_guid: string;
mine_name: string;
mine_no: string;
latitude?: string;
longitude?: string;
}
10 changes: 8 additions & 2 deletions services/common/src/interfaces/minespaceUser.interface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export interface IMinespaceUser {
user_id: number;
email_or_username: string;
keycloak_guid?: string;
sub: string;
email: string;
given_name: string;
family_name: string;
display_name: string;
bceid_username: string;
identity_provider: string;
last_logged_in: string;
mines: string[];
}

Expand Down
17 changes: 12 additions & 5 deletions services/common/src/redux/slices/minespaceSlice.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IMinespaceUser } from "@mds/common/interfaces";
import {
minespaceReducer,
createMinespaceUser,
Expand Down Expand Up @@ -40,11 +41,17 @@ jest.mock("antd", () => ({
describe("minespaceSlice", () => {
let store;

const mockMinespaceUser = {
const mockMinespaceUser: IMinespaceUser = {
user_id: 1,
email_or_username: "test@example.com",
keycloak_guid: "123-456-789",
bceid_username: "test@example.com",
mines: ["mine-guid-1", "mine-guid-2"],
sub: "sub-guid-string@bceidboth",
email: "email@email.com",
given_name: "Given",
family_name: "Family",
display_name: "Given Family",
identity_provider: "bceidboth",
last_logged_in: "2025-10-31 22:39:29.200932+00"
};

const mockMinespaceUserMine = {
Expand Down Expand Up @@ -113,7 +120,7 @@ describe("minespaceSlice", () => {
}));

const payload = {
email_or_username: "test@example.com",
bceid_username: "test@example.com",
mine_guids: ["mine-guid-1"],
};

Expand Down Expand Up @@ -144,7 +151,7 @@ describe("minespaceSlice", () => {
}));

const payload = {
email_or_username: "test@example.com",
bceid_username: "test@example.com",
mine_guids: ["mine-guid-1"],
};

Expand Down
2 changes: 1 addition & 1 deletion services/common/src/redux/slices/minespaceSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ export const getMinespaceUserEmailHash = createSelector(
(users) =>
users.reduce(
(map, fields) => ({
[fields.email_or_username]: fields,
[fields.bceid_username]: fields,
...map,
}),
{}
Expand Down
30 changes: 24 additions & 6 deletions services/common/src/tests/mocks/dataMocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -929,21 +929,39 @@ export const MINE_NO = "BLAH6666";
export const MINESPACE_USERS: IMinespaceUser[] = [
{
user_id: 1,
email_or_username: "user1@bceid",
keycloak_guid: "keycloak-guid-1",
bceid_username: "user1@bceid",
mines: [MINE_NAME_LIST.mines[0].mine_guid, MINE_NAME_LIST.mines[1].mine_guid,],
sub: "sub-guid-string@bceidboth",
email: "user1@email.com",
given_name: "Ursula",
family_name: "Underwood",
display_name: "Ursula Underwood",
identity_provider: "bceidboth",
last_logged_in: "2025-10-31 22:39:29.200932+00"
},
{
user_id: 2,
email_or_username: "user2@example.com",
keycloak_guid: "",
bceid_username: "user2@bceid",
mines: [MINE_NAME_LIST.mines[0].mine_guid,],
sub: "",
email: "",
given_name: "",
family_name: "",
display_name: "",
identity_provider: "",
last_logged_in: ""
},
{
user_id: 3,
email_or_username: "user3@bceid",
keycloak_guid: "keycloak-guid-3",
bceid_username: "user3@bceid",
mines: [MINE_NAME_LIST.mines[2].mine_guid,],
sub: "",
email: "",
given_name: "",
family_name: "",
display_name: "",
identity_provider: "",
last_logged_in: ""
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def create(cls, notification_recipient, notification_document, commit=False):
@classmethod
def create_many(cls, mine_guid, activity_type, document, idempotency_key=None, commit=True, recipients=ActivityRecipients.all_users, renotify_period_minutes=-1):
MinespaceUserMineTable = table(MinespaceUserMine.__tablename__, column('mine_guid'), column('user_id'))
MinespaceUserTable = table(MinespaceUser.__tablename__, column('email_or_username'), column('user_id'))
MinespaceUserTable = table(MinespaceUser.__tablename__, column('bceid_username'), column('user_id'))
SubscriptionTable = table(Subscription.__tablename__, column('mine_guid'), column('user_name'))

users = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def _save_new_party_business_appointment(self,
def put(self, party_guid):
if is_minespace_user():
user = bceid_username()
minespace_user = MinespaceUser.find_by_email(user + "@bceid")
minespace_user = MinespaceUser.find_by_username(user + "@bceid")
if not minespace_user:
raise BadRequest('User not found.')

Expand Down
72 changes: 56 additions & 16 deletions services/core-api/app/api/users/minespace/models/minespace_user.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import uuid, re

from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import validates
from sqlalchemy.schema import FetchedValue
from sqlalchemy.ext.hybrid import hybrid_property
from app.extensions import db

from app.api.utils.models_mixins import SoftDeleteMixin, Base
from app.api.utils.models_mixins import HistoryMixin, SoftDeleteMixin, Base, AuditMixin
from app.api.users.minespace.models.minespace_user_mine import MinespaceUserMine


class MinespaceUser(SoftDeleteMixin, Base):
class MinespaceUser(HistoryMixin, SoftDeleteMixin, AuditMixin, Base):
__tablename__ = 'minespace_user'
__versioned__ = {
'exclude': ['last_logged_in']
}

user_id = db.Column(db.Integer, primary_key=True, server_default=FetchedValue())
keycloak_guid = db.Column(UUID(as_uuid=True))
email_or_username = db.Column(db.String(100), nullable=False)
bceid_username = db.Column(db.String(), nullable=False)

# new data fields must all be nullable for legacy data
sub = db.Column(db.String())
email = db.Column(db.String())
given_name = db.Column(db.String())
family_name = db.Column(db.String())
display_name = db.Column(db.String())
identity_provider = db.Column(db.String())
bceid_user_guid = db.Column(db.String())
last_logged_in = db.Column(db.DateTime())

minespace_user_mines = db.relationship('MinespaceUserMine', backref='user', lazy='joined')

Expand All @@ -33,7 +42,7 @@ def find_by_id(cls, id):

@classmethod
def find_by_guid(cls, user_guid):
return cls.query.filter_by(keycloak_guid=user_guid).filter_by(deleted_ind=False).first()
return cls.query.filter_by(bceid_user_guid=user_guid).filter_by(deleted_ind=False).first()

@classmethod
def find_by_mine_guid(cls, mine_guid):
Expand All @@ -42,20 +51,51 @@ def find_by_mine_guid(cls, mine_guid):
).all()

@classmethod
def find_by_email(cls, email_or_username):
return cls.query.filter_by(email_or_username=email_or_username).filter_by(
def find_by_username(cls, bceid_username):
return cls.query.filter_by(bceid_username=bceid_username).filter_by(
deleted_ind=False).first()

@classmethod
def create_minespace_user(cls, email_or_username, add_to_session=True):
minespace_user = cls(email_or_username=email_or_username)
def create_minespace_user(cls, bceid_username, add_to_session=True):
minespace_user = cls(bceid_username=bceid_username)
if add_to_session:
minespace_user.save(commit=False)
return minespace_user

@validates('email_or_username')
def validate_email(self, key, email_or_username):
if not email_or_username:
@classmethod
def find_by_token_data(cls, **kwargs):
sub = kwargs.get("sub")
bceid_username = kwargs.get("bceid_username")

# if there is a user that has logged in with token data- return that user
sub_user = cls.query.filter_by(sub=sub).filter_by(deleted_ind=False).first()
if sub_user:
return sub_user

# otherwise look for an older record
bceid_user = cls.find_by_username(bceid_username)

return bceid_user

@classmethod
def update_from_token_data(cls, **kwargs):
user = cls.find_by_token_data(**kwargs)

if user is None:
return

for key, value in kwargs.items():
setattr(user, key, value)
user.save()

return user


@validates('bceid_username')
def validate_username(self, key, bceid_username):
if not bceid_username:
raise AssertionError('Identifier is not provided.')
return email_or_username
if not bceid_username.endswith('@bceid'):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a BE error when there's a bad BCeID passed in from the FE. I didn't add validation on the FE, figuring that the form will be revamped.

raise AssertionError('BCeID username must end with "@bceid".')
return bceid_username

Loading
Loading