Skip to content

Commit

Permalink
Feat: confirmation emails for joining organization (#962)
Browse files Browse the repository at this point in the history
* add migrations and graphql stuff for join confirmations for organization memberships

* start on backend logic for joining organization; some frontend as well

* add possibility to specify different email to delivery

* frontend mutation placeholder; activation code

* organization join email template type; activation link seems to work

* can refresh confirmation and send new mail

* added tests for mutations; more template data etc.

* fix failing test

* fix code style

* remove una; shorter activation code

* add organizational_email; updateUser mutation; fix few snapshots and tests

* fix test mutation type

* actually fix graphql model type name

* fix organization import parsing; convertUpdate

* registration link tiers as list

* organization join activation tests

* fix broken email delivery tests

* add example queries/mutations for user organization join/confirm

* Add form for organization registration

* fix some graphql redundancies; disable updating user organization role for now

* Add functionality for saving organization information to TMC

* Fix bug in previous commit

* Remove organization from joined organizations on TMC when leaving

* Add functionality for saving organization information to TMC

* provide missing user organization types

* move organizational_email to userorganization; service to expire confirmation links

* add organizational_identifier; logic to change organizational email

* changing organizational email on refreshing confirmation link; make some resolvers nullable

* only throw apolloerrors in resolvers; more organizational email logic; reshuffling utils

* update frontend graphql definitions

* More refactoring from id to slug in register form

* Revert "More refactoring from id to slug in register form"

This reverts commit 08c02b5.

* More refactoring from id to slug in register form (without extra stuff this time)

* fix custom scalars; refactor and shuffle some utility types

* fix bugs and unsound logic found manually testing user organization join

* more logic fixes; updating tests

* more testing user organization join/confirm

* Conditional rendering using confirmationStatus-variable

* update frontend graphql; use more utility functions in backend

* fix broken disabled organization filters

* remove console.logs; use logger in kafka log

* add code expiry date to templates

* Add view for joining organization without email verification

* Handle re-sending a registration verification email with the correct mutation

* add some typing

* fix non-working import in config

* enable json import in production as well

* use default email template for organization join; enable templating for email title and html body

* some template editor edits; filter menu context

* Add a way to update organization registration email

* code style

* collect guards in one place; fix frontend package-lock

* fix tests, snapshots, migrations

* fix test auth headers

* fix lots of backend typing; graphql dir -> schema

* fix frontend typing etc.

* add patch to nexus-plugin-prisma to force prisma version

* prevent sentry init in build

* don't include types in dist

* don't include dist in tsconfig.production

* fix course tests and snapshots, changed start/end date

* fix nullable start_date; editor tag saving

* remove ts-node-dev; remove duplicate buildusersearch

* fix sponsor input; fix user progress extra query spread order

---------

Co-authored-by: Antti Leinonen <[email protected]>
  • Loading branch information
mipyykko and Redande authored Jul 7, 2023
1 parent 51740a7 commit cf080fd
Show file tree
Hide file tree
Showing 261 changed files with 13,478 additions and 16,459 deletions.
2 changes: 1 addition & 1 deletion .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const config = {
"",
"^[./](?!graphql)",
"",
"^/graphql",
"^/(graphql|schema)",
],
importOrderParserPlugins: [
"typescript",
Expand Down
53 changes: 39 additions & 14 deletions backend/accessControl.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
import { FieldAuthorizeResolver } from "nexus/dist/plugins/fieldAuthorizePlugin"

import { Organization, User } from "@prisma/client"

import { Context } from "./context"

export enum Role {
VISITOR = 0,
USER = 1,
ADMIN = 2,
ORGANIZATION = 3, //for automated scripts, not for accounts
}
// TODO: caching?

type AuthorizeFunction = <TypeName extends string, FieldName extends string>(
...args: Parameters<FieldAuthorizeResolver<TypeName, FieldName>>
) => ReturnType<FieldAuthorizeResolver<TypeName, FieldName>>

// TODO: caching?
export const isAdmin: AuthorizeFunction = (_root, _args, ctx, _info) =>
ctx.role === Role.ADMIN
export const isUser: AuthorizeFunction = (_root, _args, ctx, _info) =>
ctx.role === Role.USER
export const isOrganization: AuthorizeFunction = (_root, _args, ctx, _info) =>
ctx.role === Role.ORGANIZATION
export const isVisitor: AuthorizeFunction = (_root, _args, ctx, _info) =>
export const isAdmin: AuthorizeFunction = (
_root,
_args,
ctx,
_info,
): ctx is Context & { user: User; organization: undefined } => {
return Boolean(ctx.user) && ctx.role === Role.ADMIN
}
export const isUser: AuthorizeFunction = (
_root,
_args,
ctx,
_info,
): ctx is Context & { user: User; organization: undefined } =>
Boolean(ctx.user) && ctx.role === Role.USER
export const isOrganization: AuthorizeFunction = (
_root,
_args,
ctx,
_info,
): ctx is Context & { user: undefined; organization: Organization } =>
Boolean(ctx.organization) && ctx.role === Role.ORGANIZATION
export const isVisitor: AuthorizeFunction = (
_root,
_args,
ctx,
_info,
): ctx is Context & { user: undefined; organization: undefined } =>
ctx.role === Role.VISITOR
export const isSameOrganization: FieldAuthorizeResolver<
"Organization",
Expand Down Expand Up @@ -51,15 +76,15 @@ export const isCourseOwner =

export const or =
(...predicates: AuthorizeFunction[]): AuthorizeFunction =>
(root, args, ctx, info) =>
predicates.some((p) => p(root, args, ctx, info))
(...params) =>
predicates.some((p) => p(...params))

export const and =
(...predicates: AuthorizeFunction[]): AuthorizeFunction =>
(root, args, ctx, info) =>
predicates.every((p) => p(root, args, ctx, info))
(...params) =>
predicates.every((p) => p(...params))

export const not =
(fn: AuthorizeFunction): AuthorizeFunction =>
(root, args, ctx, info) =>
!fn(root, args, ctx, info)
(...params) =>
!fn(...params)
2 changes: 1 addition & 1 deletion backend/api/routes/__test__/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ describe("API", () => {
it("errors on non-existent secret", async () => {
return postCompletions({
data: { foo: 1 },
headers: { Authorization: "Basic koira" },
headers: { Authorization: "Basic ronsu" },
})
.then(() => fail())
.catch(({ response }) => {
Expand Down
2 changes: 1 addition & 1 deletion backend/api/routes/abStudio/abEnrollments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Request, Response, Router } from "express"

import { AbEnrollment } from "@prisma/client"

import { getUser } from "../../../util/server-functions"
import { ApiContext } from "../../types"
import { getUser } from "../../utils"

export function abEnrollmentRouter(ctx: ApiContext) {
async function abEnrollmentGet(
Expand Down
26 changes: 15 additions & 11 deletions backend/api/routes/abStudio/abStudies.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Request, Response, Router } from "express"

import { requireAdmin } from "../../../util/server-functions"
import { isDefined } from "../../../util"
import { ApiContext } from "../../types"
import { requireAdmin } from "../../utils"

export function abStudiesRouter(ctx: ApiContext) {
async function abStudiesGet(req: Request<{ id?: string }>, res: Response) {
const { prisma } = ctx
const adminRes = await requireAdmin(ctx)(req, res)

if (adminRes !== true) {
return adminRes
if (adminRes.isErr()) {
return adminRes.error
}

const { id } = req.params
Expand All @@ -25,23 +26,26 @@ export function abStudiesRouter(ctx: ApiContext) {
return res.status(200).json(abStudy)
}

async function abStudiesPost(req: Request, res: Response) {
async function abStudiesPost(
req: Request<any, any, { name: string; group_count: number }>,
res: Response,
) {
const { prisma } = ctx
const adminRes = await requireAdmin(ctx)(req, res)

if (adminRes !== true) {
return adminRes
if (adminRes.isErr()) {
return adminRes.error
}

const { name, group_count } = req.body

if (!name || !group_count) {
if (!isDefined(name) || !isDefined(group_count)) {
return res
.status(400)
.json({ message: "must provide name and group count" })
}

if (parseInt(group_count) < 1) {
if (group_count < 1) {
return res.status(400).json({ error: "group_count must be 1 or more" })
}

Expand All @@ -60,14 +64,14 @@ export function abStudiesRouter(ctx: ApiContext) {
}

async function abStudiesUsersPost(
req: Request<{ id: string }>,
req: Request<{ id: string }, any, { users: Array<number> }>,
res: Response,
) {
const { prisma } = ctx
const adminRes = await requireAdmin(ctx)(req, res)

if (adminRes !== true) {
return adminRes
if (adminRes.isErr()) {
return adminRes.error
}

const { users } = req.body
Expand Down
55 changes: 34 additions & 21 deletions backend/api/routes/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@prisma/client"

import { generateUserCourseProgress } from "../../bin/kafkaConsumer/common/userCourseProgress/generateUserCourseProgress"
import { err } from "../../util/result"
import { err, isDefined } from "../../util"
import { ApiContext, Controller } from "../types"

const languageMap: Record<string, string> = {
Expand All @@ -28,6 +28,18 @@ interface RegisterCompletionInput {
tier?: number
registration_date?: string
}
interface Tier {
tier: number
name: string
course_id: string
adjacent: Array<Tier>
}

interface TierData {
name: string
link: string | null
}

export class CompletionController extends Controller {
constructor(override readonly ctx: ApiContext) {
super(ctx)
Expand Down Expand Up @@ -81,6 +93,8 @@ export class CompletionController extends Controller {

const stream = sql.stream().pipe(JSONStream.stringify()).pipe(res)
req.on("close", stream.end.bind(stream))

return // NOSONAR
}

completionInstructions = async (
Expand Down Expand Up @@ -121,9 +135,6 @@ export class CompletionController extends Controller {
const { user } = getUserResult.value
const { slug } = req.params

// TODO: typing
const tierData: any = []

const course = await this.getCourseKnex({ slug })

if (!course) {
Expand All @@ -147,45 +158,47 @@ export class CompletionController extends Controller {
// - as it's now only used in BAI, shouldn't be a problem?
const tiers = (
await knex
.select<any, OpenUniversityRegistrationLink[]>("tiers")
.select<"tiers", OpenUniversityRegistrationLink[]>("tiers")
.from("open_university_registration_link")
.where("course_id", course.id)
)?.[0].tiers
)?.[0].tiers as Array<Tier | null> | null

const tierData: Array<TierData> = []

if (tiers) {
const t: any = tiers
const t = tiers.filter(isDefined)

for (let i = 0; i < t.length; i++) {
if (t[i].tier === completion.tier) {
for (const element of t) {
if (element.tier === completion.tier) {
const tierRegister = (
await knex
.select<any, OpenUniversityRegistrationLink[]>("link")
.from("open_university_registration_link")
.where("course_id", t[i].course_id)
.where("course_id", element.course_id)
)?.[0]

tierData.push({ name: t[i].name, link: tierRegister.link })
tierData.push({ name: element.name, link: tierRegister.link })

if (t[i].adjacent) {
for (let j = 0; j < t[i].adjacent.length; j++) {
if (element.adjacent) {
for (const adjacentElement of element.adjacent) {
const adjRegister = (
await knex
.select<any, OpenUniversityRegistrationLink[]>("link")
.from("open_university_registration_link")
.where("course_id", t[i].adjacent[j].course_id)
.where("course_id", adjacentElement.course_id)
)?.[0]

tierData.push({
name: t[i].adjacent[j].name,
name: adjacentElement.name,
link: adjRegister.link,
})
}
}
}
}

return res.status(200).json({ tierData })
}

return res.status(200).json({ tierData })
}

updateCertificateId = async (
Expand All @@ -204,8 +217,8 @@ export class CompletionController extends Controller {
const { prisma } = this.ctx
const adminRes = await this.requireAdmin(req, res)

if (adminRes !== true) {
return adminRes
if (adminRes.isErr()) {
return adminRes.error
}

const { slug } = req.params
Expand Down Expand Up @@ -288,8 +301,8 @@ export class CompletionController extends Controller {
const { prisma } = this.ctx
const adminRes = await this.requireAdmin(req, res)

if (adminRes !== true) {
return adminRes
if (adminRes.isErr()) {
return adminRes.error
}

const { course_id, slug, user_id, user_upstream_id } = req.body
Expand Down
Loading

0 comments on commit cf080fd

Please sign in to comment.