Skip to content

Commit

Permalink
feat(API): allow auto-provisioning of users (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner authored Oct 19, 2024
1 parent c9e471f commit ffebb9b
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 1 deletion.
58 changes: 58 additions & 0 deletions server/controllers/AuthController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type {
ResetPasswordBody,
TokenAuthenticationVerificationResult,
CheckCASInterruptQuery,
AutoProvisionUserBody,
} from '../types/auth';
import { LoginEventController } from '@server/controllers/LoginEventController';

Expand Down Expand Up @@ -420,6 +421,7 @@ export class AuthController {
legacy: false,
ip_address: ip,
verify_status: 'not_attempted',
registration_type: 'self',
});
const verifyCode = await verificationController.createVerification(
newUser.get('uuid'),
Expand Down Expand Up @@ -671,6 +673,7 @@ export class AuthController {
verify_status: 'not_attempted',
external_idp: userData.clientname,
last_access: new Date(),
registration_type: 'self'
});
} else {
await foundUser.update({
Expand All @@ -688,6 +691,61 @@ export class AuthController {
return res.status(200).send({});
}

/**
* Creates a new user via a request from an authorized LibreOne application. Used to generate user
* accounts on-demand for scenarios like Canvas LTI. In these cases, LibreOne is still the identity provider,
* so this would not be handled in the same manner as external identity providers.
*
* @param req - Incoming API request.
* @param res - Outgoing API response.
* @returns The fulfilled API response.
*/
public async autoProvisionUser(req: Request, res: Response): Promise<Response> {
const { email, first_name, last_name, user_type, time_zone } = req.body as AutoProvisionUserBody;

const foundUser = await User.findOne({
where: { email }
});

let resultingUUID = '';

if (!foundUser) {
resultingUUID = uuidv4();
await User.create({
uuid: resultingUUID,
email,
first_name: first_name?.trim() ?? DEFAULT_FIRST_NAME,
last_name: last_name?.trim() ?? DEFAULT_LAST_NAME,
user_type,
time_zone,
avatar: DEFAULT_AVATAR,
disabled: false,
expired: false,
legacy: false,
verify_status: 'not_attempted',
last_access: new Date(),
registration_type: 'api',
registration_complete: true,
});
} else {
resultingUUID = foundUser.uuid
await foundUser.update({
email,
first_name: first_name?.trim() ?? DEFAULT_FIRST_NAME,
last_name: last_name?.trim() ?? DEFAULT_LAST_NAME,
last_access: new Date(),
});
}

if(!resultingUUID) {
return errors.internalServerError(res);
}

return res.status(200).send({
central_identity_id: resultingUUID
});
}

public async checkCASInterrupt(req: Request, res: Response): Promise<Response> {
const { registeredService, username } = req.query as CheckCASInterruptQuery;

Expand Down
3 changes: 3 additions & 0 deletions server/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ export class User extends Model {
@Column(DataType.DATE)
declare last_password_change: Date;

@Column(DataType.ENUM('self', 'api'))
declare registration_type: string;

@BelongsToMany(() => Application, () => UserApplication)
applications?: Array<Application & { UserApplication: UserApplication }>;

Expand Down
8 changes: 8 additions & 0 deletions server/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ authRouter.route('/external-provision').post(
catchInternal((req, res) => controller.createUserFromExternalIdentityProvider(req, res)),
);

authRouter.route('/auto-provision').post(
verifyAPIAuthentication,
ensureActorIsAPIUser,
ensureAPIUserHasPermission(['users:write']),
validate(AuthValidator.autoProvisionUserSchema, 'body'),
catchInternal((req, res) => controller.autoProvisionUser(req, res)),
)

authRouter.route('/cas-interrupt-check').get(
verifyAPIAuthentication,
ensureActorIsAPIUser,
Expand Down
8 changes: 8 additions & 0 deletions server/types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,11 @@ export type ResetPasswordBody = {
token: string;
password: string;
};

export type AutoProvisionUserBody = {
email: string;
first_name: string;
last_name: string;
user_type: 'student' | 'instructor';
time_zone: string;
};
10 changes: 9 additions & 1 deletion server/validators/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import joi from 'joi';
import { passwordValidator } from './shared';
import { passwordValidator, timeZoneValidator } from './shared';

export const registerSchema = joi.object({
email: joi.string().email().required(),
password: passwordValidator,
});

export const autoProvisionUserSchema = joi.object({
email: joi.string().email().required(),
first_name: joi.string().min(1).max(100).trim().required(),
last_name: joi.string().min(1).max(100).trim().required(),
user_type: joi.string().valid('student', 'instructor').required(),
time_zone: timeZoneValidator.required(),
});

export const verifyEmailSchema = joi.object({
email: joi.string().email().required(),
code: joi.number().integer().min(100000).max(999999).required(),
Expand Down

0 comments on commit ffebb9b

Please sign in to comment.