Skip to content

Commit

Permalink
Merge pull request #9 from icefoganalytics/main
Browse files Browse the repository at this point in the history
Updates from Icefog
  • Loading branch information
datajohnson authored Mar 8, 2024
2 parents af5aa86 + 1bbd6ec commit a099e71
Show file tree
Hide file tree
Showing 16 changed files with 300 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class AddRandomAccessRequestsController extends BaseController {
firstName,
lastName,
position: faker.person.jobTitle(),
lastEmployeeDirectorySyncAt: faker.date.recent(),
lastSyncSuccessAt: faker.date.recent(),
})

await Role.create({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Migration } from "@/db/umzug"
import { MssqlSimpleTypes } from "@/db/utils/mssql-simple-types"

export const up: Migration = async ({ context: queryInterface }) => {
await queryInterface.addColumn("users", "last_sync_success_at", {
type: MssqlSimpleTypes.DATETIME2(0),
allowNull: true,
})
await queryInterface.sequelize.query(/* sql */ `
UPDATE users
SET last_sync_success_at = last_employee_directory_sync_at
`)
await queryInterface.removeColumn("users", "last_employee_directory_sync_at")

await queryInterface.addColumn("users", "last_sync_failure_at", {
type: MssqlSimpleTypes.DATETIME2(0),
allowNull: true,
})
}

export const down: Migration = async ({ context: queryInterface }) => {
await queryInterface.removeColumn("users", "last_sync_failure_at")

await queryInterface.addColumn("users", "last_employee_directory_sync_at", {
type: MssqlSimpleTypes.DATETIME2(0),
allowNull: true,
})
await queryInterface.sequelize.query(/* sql */ `
UPDATE users
SET last_employee_directory_sync_at = last_sync_success_at
`)
await queryInterface.removeColumn("users", "last_sync_success_at")
}
37 changes: 13 additions & 24 deletions api/src/middlewares/authorization-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { type NextFunction, type Response } from "express"
import { type Request as JwtRequest } from "express-jwt"
import { isNil } from "lodash"

import { User } from "@/models"
import { Users } from "@/services"

import auth0Integration, { Auth0PayloadError } from "@/integrations/auth0-integration"
import { Role, User } from "@/models"

export type AuthorizationRequest = JwtRequest & {
currentUser?: User
Expand All @@ -11,28 +14,8 @@ export type AuthorizationRequest = JwtRequest & {
async function findOrCreateUserFromAuth0Token(token: string): Promise<User> {
const { auth0Subject, email, firstName, lastName } = await auth0Integration.getUserInfo(token)

// TODO: move to ensure user service
const [user, created] = await User.findOrCreate({
let user = await User.findOne({
where: { auth0Subject },
defaults: {
auth0Subject,
email: email,
firstName,
lastName,
},
})
await Role.findOrCreate({
where: {
userId: user.id,
role: Role.Types.USER,
},
defaults: {
userId: user.id,
role: Role.Types.USER,
},
})

await user.reload({
include: [
"roles",
{
Expand All @@ -42,8 +25,14 @@ async function findOrCreateUserFromAuth0Token(token: string): Promise<User> {
],
})

if (created) {
console.log(`CREATED USER FOR ${email}: ${JSON.stringify(user.dataValues)}`)
if (isNil(user)) {
user = await Users.CreateService.perform({
auth0Subject,
email,
firstName,
lastName,
})
console.log(`CREATED USER FOR User#${user.id} with ${email}`)
}

return user
Expand Down
21 changes: 17 additions & 4 deletions api/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export class User extends BaseModel<InferAttributes<User>, InferCreationAttribut
declare firstName: string | null
declare lastName: string | null
declare position: string | null
declare lastEmployeeDirectorySyncAt: Date | null
declare lastSyncSuccessAt: Date | null
declare lastSyncFailureAt: Date | null
declare createdAt: CreationOptional<Date>
declare updatedAt: CreationOptional<Date>
declare deletedAt: CreationOptional<Date>
Expand Down Expand Up @@ -155,13 +156,21 @@ export class User extends BaseModel<InferAttributes<User>, InferCreationAttribut
return this.groupMembership?.unit
}

/**
* NOTE: Blocks sync if there was a sync failure, requires manual intervention
* to re-enable syncing.
*/
isTimeToSyncWithEmployeeDirectory(): NonAttribute<boolean> {
if (this.lastEmployeeDirectorySyncAt === null) {
if (this.lastSyncFailureAt !== null) {
return false
}

if (this.lastSyncSuccessAt === null) {
return true
}

const current = DateTime.utc()
const lastSyncDate = DateTime.fromJSDate(this.lastEmployeeDirectorySyncAt, { zone: "utc" })
const lastSyncDate = DateTime.fromJSDate(this.lastSyncSuccessAt, { zone: "utc" })

return !current.hasSame(lastSyncDate, "day")
}
Expand Down Expand Up @@ -195,7 +204,11 @@ User.init(
type: DataTypes.STRING(100),
allowNull: true,
},
lastEmployeeDirectorySyncAt: {
lastSyncSuccessAt: {
type: DataTypes.DATE,
allowNull: true,
},
lastSyncFailureAt: {
type: DataTypes.DATE,
allowNull: true,
},
Expand Down
2 changes: 1 addition & 1 deletion api/src/serializers/datasets/show-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export class ShowSerializer extends BaseSerializer<Dataset> {
"headerValue",
"jmesPathTransform",
]),
visualizationControl: this.record.visualizationControl,
})
}

Expand Down Expand Up @@ -73,6 +72,7 @@ export class ShowSerializer extends BaseSerializer<Dataset> {
owner: this.record.owner,
stewardship: this.record.stewardship,
tags: this.record.tags,
visualizationControl: this.record.visualizationControl,

// magic fields
currentUserAccessGrant,
Expand Down
2 changes: 1 addition & 1 deletion api/src/serializers/user-serializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class UserSerializers extends BaseSerializer<User> {
"firstName",
"lastName",
"position",
"lastEmployeeDirectorySyncAt",
"lastSyncSuccessAt",
"createdAt",
"updatedAt",
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class YukonGovernmentDirectorySyncService extends BaseService {
const isDivision = branch === null && unit === null
const isBranch = unit === null

let [userGroup1] = await UserGroup.findOrCreate({
const [userGroup1] = await UserGroup.findOrCreate({
where: {
name: department,
type: UserGroupTypes.DEPARTMENT,
Expand Down
107 changes: 107 additions & 0 deletions api/src/services/users/create-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { isEmpty, isNil } from "lodash"

import db, { Role, User, UserGroup, UserGroupMembership } from "@/models"
import { DEFAULT_ORDER } from "@/models/user-groups"

import { Users } from "@/services"
import BaseService from "@/services/base-service"

type GroupMembershipAttributes = Partial<UserGroupMembership>
type Attributes = Partial<User> & {
groupMembershipAttributes?: GroupMembershipAttributes
}

export class CreateService extends BaseService {
private attributes: Partial<User>
private groupMembershipAttributes?: GroupMembershipAttributes

constructor({ groupMembershipAttributes, ...attributes }: Attributes) {
super()
this.attributes = attributes
this.groupMembershipAttributes = groupMembershipAttributes
}

async perform(): Promise<User> {
const { auth0Subject, email, firstName, lastName, ...optionalAttributes } = this.attributes

if (isNil(auth0Subject) || isEmpty(auth0Subject)) {
throw new Error("auth0Subject is required")
}

if (isNil(email) || isEmpty(email)) {
throw new Error("email is required")
}

if (isEmpty(firstName)) {
throw new Error("firstName is required")
}

if (isEmpty(lastName)) {
throw new Error("lastName is required")
}

return db.transaction(async () => {
const user = await User.create({
auth0Subject,
email,
firstName,
lastName,
...optionalAttributes,
})

await Role.create({
userId: user.id,
role: Role.Types.USER,
})

await this.ensureUserGroupMembership(user, this.groupMembershipAttributes)

return user.reload({
include: [
"roles",
{
association: "groupMembership",
include: ["department", "division", "branch", "unit"],
},
],
})
})
}

private async ensureUserGroupMembership(
user: User,
groupMembershipAttributes?: GroupMembershipAttributes
) {
if (!isEmpty(groupMembershipAttributes)) {
return UserGroupMembership.create({
userId: user.id,
...groupMembershipAttributes,
})
}

await Users.YukonGovernmentDirectorySyncService.perform(user)

if (!isEmpty(user.groupMembership)) {
return user.groupMembership
}

const [defaultGroup] = await UserGroup.findOrCreate({
where: {
type: UserGroup.Types.DEPARTMENT,
name: "Unassigned",
},
defaults: {
name: "Unassigned",
type: UserGroup.Types.DEPARTMENT,
order: DEFAULT_ORDER,
},
})

return UserGroupMembership.create({
userId: user.id,
departmentId: defaultGroup.id,
})
}
}

export default CreateService
1 change: 1 addition & 0 deletions api/src/services/users/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { CreateService } from "./create-service"
export { YukonGovernmentDirectorySyncService } from "./yukon-government-directory-sync-service"
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export class YukonGovernmentDirectorySyncService extends BaseService {
unitId: userGroup4?.id,
})

this.user.update({
lastSyncSuccessAt: new Date(),
lastSyncFailureAt: null,
})

return this.user.reload({
include: [
"roles",
Expand All @@ -71,7 +76,7 @@ export class YukonGovernmentDirectorySyncService extends BaseService {
} catch (error) {
console.log("Failed to sync user with yukon government directory", error)
await this.user.update({
lastEmployeeDirectorySyncAt: new Date(),
lastSyncFailureAt: new Date(),
})
return this.user
}
Expand Down
2 changes: 1 addition & 1 deletion api/tests/factories/user-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const userFactory = BaseFactory.define<User, TransientParam>(
firstName,
lastName,
position: faker.person.jobTitle(),
lastEmployeeDirectorySyncAt: faker.date.recent(),
lastSyncFailureAt: faker.date.recent(),
})
}
)
Expand Down
28 changes: 19 additions & 9 deletions web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@
<!--
NOTE: current user will always be defined when the authenticated router view loads.
-->
<router-view v-else-if="isReady" />
<PageLoader v-else />
<router-view v-else-if="isReady || isErrored" />
<PageLoader
v-else-if="isReadyAuth0"
message="Fetching and syncing user"
/>
<PageLoader
v-else
message="Checking authentication status ..."
/>
<AppSnackbar />
</v-app>
</template>

<script lang="ts" setup>
import { computed, watch } from "vue"
import { computed, ref, watch } from "vue"
import { useAuth0 } from "@auth0/auth0-vue"
import { useRoute } from "vue-router"
import { useRoute, useRouter } from "vue-router"
import useCurrentUser from "@/use/use-current-user"
import PageLoader from "@/components/PageLoader.vue"
import AppSnackbar from "@/components/AppSnackbar.vue"
Expand All @@ -25,10 +33,12 @@ const isUnauthenticatedRoute = computed(() => route.meta.requiresAuth === false)
const { isLoading: isLoadingAuth0, isAuthenticated } = useAuth0()
const isReadyAuth0 = computed(() => !isLoadingAuth0.value && isAuthenticated.value)
const { isReady: isReadyCurrentUser, ensure } = useCurrentUser()
const { isReady: isReadyCurrentUser, fetch } = useCurrentUser()
const isReady = computed(() => isReadyAuth0.value && isReadyCurrentUser.value)
const isErrored = ref(false)
const router = useRouter()
watch(
() => isReadyAuth0.value,
async (newIsReadyAuth0) => {
Expand All @@ -37,11 +47,11 @@ watch(
if (newIsReadyAuth0 === true) {
try {
await ensure()
await fetch()
} catch (error) {
console.log("Failed to ensure current user:", error)
// Toast/snack Please contact support ...
// logout?
console.log("Failed to load current user:", error)
isErrored.value = true
router.push({ name: "UnauthorizedPage" })
}
}
},
Expand Down
Loading

0 comments on commit a099e71

Please sign in to comment.