Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add baseDomain to user #298

Merged
merged 1 commit into from
Mar 7, 2023
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
11 changes: 6 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Development](#development)
- [System prerequisites](#system-prerequisites)
- [Dev environment set up](#dev-environment-set-up)
- [SAML Accounts to use in Dev](#saml-accounts-to-use-in-dev)
- [`.env` and `./dev-secrets/*`](#env-and-dev-secrets)
- [Usage](#usage)
- [Pull requests](#pull-requests)
Expand Down Expand Up @@ -62,11 +63,11 @@ $ npm run db:studio

Our IDP is configured with a few accounts that exist for testing, the usernames and passwords to use are as follows:

| user | pass |
| ----------- | --------- |
| user1 | user1pass |
| user2 | user2pass |
| lippersheyh | telescope |
| user | pass |
| -------- | --------- |
| user1 | user1pass |
| user2 | user2pass |
| han.solo | starchart |

They can be configured in `./config/simplesamlphp-users`

Expand Down
24 changes: 20 additions & 4 deletions app/components/dns-record/form.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
import { AddIcon, InfoIcon } from '@chakra-ui/icons';
import { Button, Input, Select, Textarea, Tooltip, VStack } from '@chakra-ui/react';
import {
Button,
Input,
InputGroup,
InputRightAddon,
Select,
Textarea,
Tooltip,
VStack,
} from '@chakra-ui/react';
import { Form } from '@remix-run/react';

import { useUser } from '~/utils';
import FormField from './form-field';

interface DnsRecordFormProps {
typeError?: string; // Error for 'Type' field
}

export default function DnsRecordForm({ typeError }: DnsRecordFormProps) {
const user = useUser();

return (
<Form className="domain-form" method="post">
<VStack maxW="xl" spacing="2">
<FormField label="Domain Name" isRequired={true}>
<Input name="name" />
<Tooltip label="Enter domain name for the DNS Record">
<FormField label="Record Name" isRequired={true}>
<InputGroup>
<Input name="name" />
<InputRightAddon children={`.${user.baseDomain}`} />
</InputGroup>
<Tooltip label="Enter a name for the DNS Record: name">
<InfoIcon color="#d9d9d9" fontSize="xl" />
</Tooltip>
</FormField>
Expand Down
80 changes: 50 additions & 30 deletions app/models/user.server.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,61 @@
import type { User } from '@prisma/client';

import { prisma } from '~/db.server';
import logger from '~/lib/logger.server';

export type { User } from '@prisma/client';
import type { User as PrismaUser } from '@prisma/client';

export async function getUserByUsername(username: User['username']) {
return prisma.user.findUnique({ where: { username } });
export interface User extends PrismaUser {
// the base domain to use for all DNS records created by this user
// (e.g., jsmith.starchart.com with records using *.jsmith.starchart.com)
baseDomain: string;
}

export async function createUser(
username: User['username'],
firstName: User['firstName'],
lastName: User['lastName'],
email: User['email']
) {
return prisma.user.create({
data: {
username,
firstName,
lastName,
email,
},
});
/**
* Remove invalid/unwanted characters from a username. In the case
* of faculty/admins, we will have a `.` in the username, which we
* don't want to use as part of domain names.
* @param username The user's username (e.g., `jsmith` or `john.smith`)
*/
function cleanUsername(username: PrismaUser['username']) {
return username.replace(/\./g, '');
}

export async function updateUserByUsername(
username: User['username'],
firstName?: User['firstName'],
lastName?: User['lastName'],
email?: User['email']
/**
* Create the domain for a user, using their username
* @param username The user's username (e.g., `jsmith` or `john.smith`)
* @returns string jsmith -> jsmith.starchart.com
*/
function buildUserBaseDomain(username: PrismaUser['username']) {
return `${cleanUsername(username)}.${process.env.ROOT_DOMAIN}`;
}

export async function getUserByUsername(username: PrismaUser['username']) {
const user = await prisma.user.findUnique({ where: { username } });
if (!user) {
return null;
}

// Decorate with the user's base domain as well
return { ...user, baseDomain: buildUserBaseDomain(username) };
}

/**
* Check whether or not a user exists already with the given username
* @param username
* @returns boolean
*/
export async function checkUsernameExists(username: PrismaUser['username']) {
const user = await prisma.user.findUnique({ where: { username } });
return user !== null;
}

export async function createUser(
username: PrismaUser['username'],
firstName: PrismaUser['firstName'],
lastName: PrismaUser['lastName'],
email: PrismaUser['email']
) {
return prisma.user.update({
where: { username },
logger.info(`Creating new user ${username}`);
return prisma.user.create({
data: {
username,
firstName,
Expand All @@ -40,7 +64,3 @@ export async function updateUserByUsername(
},
});
}

export async function deleteUserByUsername(username: User['username']) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, do we not need the delete or update functionality here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We aren't the source of truth on user info, we just store it for lookup purposes. We don't really manage user info. I can't think of a case where we'd do update or delete, can you? Maybe when we add the admin UI, but currently this is dead code, and dead code should get removed.

return prisma.user.delete({ where: { username } });
}
12 changes: 9 additions & 3 deletions app/routes/__index/domains/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { redirect, typedjson, useTypedActionData } from 'remix-typedjson';

import DnsRecordForm from '~/components/dns-record/form';
import { createRecord } from '~/models/record.server';
import { requireUsername } from '~/session.server';
import { requireUser } from '~/session.server';

import type { ActionArgs } from '@remix-run/node';
import type { ZodError } from 'zod';
Expand All @@ -16,7 +16,7 @@ function errorForField(error: ZodError, field: string) {
}

export const action = async ({ request }: ActionArgs) => {
const username = await requireUsername(request);
const user = await requireUser(request);

// Create a Zod schema for validation
// Optional is not needed as we get '' if nothing is entered
Expand All @@ -40,9 +40,15 @@ export const action = async ({ request }: ActionArgs) => {
});
}

// Update the DNS record's name with the user's full base domain.
// In the UI, we only ask the user to give us the first part of
// the domain name (e.g., `foo` in `foo.username.root.com`).
const { data } = newDnsRecordParams;
data.name = `${data.name}.${user.baseDomain}`;

// Create a pending record... for now
// This will most likely be replaced by some other logic
const record = await createRecord({ username, ...newDnsRecordParams.data, status: 'pending' });
const record = await createRecord({ username: user.username, ...data, status: 'pending' });
return redirect(`/domains/${record.id}`);
};

Expand Down
10 changes: 4 additions & 6 deletions app/routes/login/callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ActionArgs } from '@remix-run/node';
import { createUserSession } from '~/session.server';
import { parseLoginResponse } from '~/lib/saml.server';
import { redirect } from '@remix-run/node';
import { createUser, getUserByUsername } from '~/models/user.server';
import { createUser, checkUsernameExists } from '~/models/user.server';

/* This is the post route that the SAML response is bound to. It comes back as formData.
We attempt to extract the SAML response into a json format that we can then use:
Expand Down Expand Up @@ -56,12 +56,10 @@ export const action = async ({ request }: ActionArgs) => {
}
const returnTo = relayState ? relayState : '/';
const username = samlResponse.attributes.sAMAccountName;
// get or create user
let user = await getUserByUsername(username);

// If not create one
if (!user) {
user = await createUser(
// If this user has never logged in before, add to our system
if (!(await checkUsernameExists(username))) {
await createUser(
username,
samlResponse.attributes['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'],
samlResponse.attributes['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'],
Expand Down
12 changes: 6 additions & 6 deletions config/simplesamlphp-users.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@
'sAMAccountName'=> 'user2',
'http://schemas.microsoft.com/identity/claims/displayname' => 'Galileo Galilei',
),
'lippersheyh:telescope' => array(
'han.solo:starchart' => array(
'uid' => array('2'),
'eduPersonAffiliation' => array('group2'),
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' => 'hansolo@myseneca.ca',
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' => 'han.solo@myseneca.ca',
'email' => '[email protected]',
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' => 'Hans',
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => 'Lippershey',
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'=> 'hansolo@myseneca.ca',
'sAMAccountName'=> 'hansolo',
'http://schemas.microsoft.com/identity/claims/displayname' => 'Hans Lippershey',
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => 'Solo',
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'=> 'han.solo@myseneca.ca',
'sAMAccountName'=> 'han.solo',
'http://schemas.microsoft.com/identity/claims/displayname' => 'Hans Solo',

),
),
Expand Down