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

🏫 Organizations and Roles #961

Merged
merged 6 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
37 changes: 37 additions & 0 deletions next/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,55 @@ model User {
email String? @unique
emailVerified DateTime?
image String?
superAdmin Boolean @default(false) @map("super_admin")

createDate DateTime @default(now())

accounts Account[]
sessions Session[]
runs Run[]
Agent Agent[]
UserRole UserRole[]
Copy link
Contributor

Choose a reason for hiding this comment

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

a bit in-consistent with casing, and plurals

Should we call this userRoles?


@@index([email])
@@index([createDate])
}

model Organization {
id String @id @default(cuid())
name String @unique

created_by String
Copy link
Contributor

Choose a reason for hiding this comment

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

this is a username? email?

Thoughts on making this point to a owner user?

create_date DateTime @default(now())
update_date DateTime? @updatedAt
delete_date DateTime?

UserRole UserRole[]

@@index([name])
@@map("organization")
}

enum RoleName {
ADMIN
MEMBER
}

model UserRole {
id String @id @default(cuid())
user_id String
organization_id String
role RoleName

user User @relation(fields: [user_id], references: [id], onDelete: NoAction)
organization Organization? @relation(fields: [organization_id], references: [id], onDelete: NoAction)

@@unique([user_id, organization_id])
@@index([user_id])
@@index([organization_id])
@@map("user_role")
}

model VerificationToken {
identifier String
token String @unique
Expand Down
17 changes: 15 additions & 2 deletions next/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Session } from "next-auth";
import { signIn, signOut, useSession } from "next-auth/react";
import { useEffect } from "react";
import { useRouter } from "next/router";

type Provider = "google" | "github" | "discord";

Expand All @@ -11,14 +12,26 @@ interface Auth {
session: Session | null;
}

export function useAuth({ protectedRoute } = { protectedRoute: false }): Auth {
interface UseAuthOptions {
protectedRoute?: boolean;
isAllowed?: (user: Session) => boolean;
}

export function useAuth(
{ protectedRoute, isAllowed }: UseAuthOptions = { protectedRoute: false, isAllowed: () => true }
Comment on lines +20 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

We specify defaults but the fields are also nullable?

): Auth {
const { data: session, status } = useSession();
const { push } = useRouter();

useEffect(() => {
if (protectedRoute && status === "unauthenticated") {
handleSignIn().catch(console.error);
}
}, [protectedRoute, status]);

if (protectedRoute && status === "authenticated" && isAllowed && !isAllowed(session)) {
void push("/404").catch(console.error);
}
}, [protectedRoute, isAllowed, status, session, push]);

const handleSignIn = async () => {
await signIn();
Expand Down
17 changes: 12 additions & 5 deletions next/src/pages/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import Image from "next/image";
import { useRouter } from "next/router";
import { getServerSession } from "next-auth/next";
import type { BuiltInProviderType } from "next-auth/providers";
import { getProviders, signIn, useSession } from "next-auth/react";
import type { ClientSafeProvider } from "next-auth/react";
import { getProviders, signIn, useSession } from "next-auth/react";
import type { LiteralUnion } from "next-auth/react/types";
import React, { useState } from "react";
import { FaDiscord, FaGithub, FaGoogle } from "react-icons/fa";

import FadeIn from "../components/motions/FadeIn";
import { authOptions } from "../server/auth/auth";
import Checkbox from "../ui/checkbox";
import Input from "../ui/input";

const SignIn = ({ providers }: { providers: Provider }) => {
Expand Down Expand Up @@ -69,24 +70,30 @@ const SignIn = ({ providers }: { providers: Provider }) => {

const InsecureSignin = () => {
const [usernameValue, setUsernameValue] = useState("");
const adminState = useState(false);

return (
<div>
<div className="flex flex-col">
<Input
value={usernameValue}
onChange={(e) => setUsernameValue(e.target.value)}
placeholder="Enter Username"
type="text"
name="Username Field"
/>
<Checkbox model={adminState} label="Admin" className="mt-2" />
<button
onClick={() => {
if (!usernameValue) return;

signIn("credentials", { callbackUrl: "/", name: usernameValue }).catch(console.error);
signIn("credentials", {
callbackUrl: "/",
name: usernameValue,
superAdmin: adminState[0],
}).catch(console.error);
}}
className={clsx(
"mb-4 mt-4 flex items-center rounded-md bg-white px-10 py-3 text-sm font-semibold text-black sm:text-base",
"transition-colors duration-300 hover:bg-gray-200",
"mb-4 mt-4 flex items-center rounded-md bg-white px-10 py-3 text-sm font-semibold text-black transition-colors duration-300 hover:bg-gray-200 sm:text-base",
!usernameValue && "cursor-not-allowed"
)}
>
Expand Down
4 changes: 2 additions & 2 deletions next/src/pages/workflow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import type { WorkflowMeta } from "../../services/workflow/workflowApi";
import WorkflowApi from "../../services/workflow/workflowApi";
import { languages } from "../../utils/languages";


const WorkflowList: NextPage = () => {
const { session } = useAuth({ protectedRoute: true });
const { session } = useAuth({ protectedRoute: true, isAllowed: (s) => s.user.superAdmin });

const router = useRouter();

const api = new WorkflowApi(session?.accessToken);
Expand Down
7 changes: 5 additions & 2 deletions next/src/server/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { IncomingMessage, ServerResponse } from "http";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import merge from "lodash/merge";
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";
import type { AuthOptions } from "next-auth";
import { getServerSession } from "next-auth";
import type { Adapter } from "next-auth/adapters";

import { authOptions as prodOptions } from "./auth";
Expand All @@ -16,7 +16,10 @@ const commonOptions: Partial<AuthOptions> & { adapter: Adapter } = {
adapter: PrismaAdapter(prisma),
callbacks: {
async session({ session, user }) {
if (session.user) session.user.id = user.id;
if (session.user) {
session.user.id = user.id;
session.user.superAdmin = user.superAdmin;
}

session.accessToken = (
await prisma.session.findFirstOrThrow({
Expand Down
25 changes: 15 additions & 10 deletions next/src/server/auth/local-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import Credentials from "next-auth/providers/credentials";
import { v4 } from "uuid";
import { z } from "zod";



const monthFromNow = () => {
const now = new Date(Date.now());
return new Date(now.setMonth(now.getMonth() + 1));
Expand Down Expand Up @@ -38,25 +36,32 @@ export const options = (
name: "Username, Development Only (Insecure)",
credentials: {
name: { label: "Username", type: "text" },
superAdmin: { label: "SuperAdmin", type: "text" },
},
async authorize(credentials, req) {
if (!credentials) return null;

const creds = z
.object({
name: z.string().min(1),
superAdmin: z.preprocess((str) => str === "true", z.boolean()).default(false),
})
.parse(credentials);

const user = await adapter.getUserByEmail(creds.name);
if (user) return user;

return adapter.createUser({
name: creds.name,
email: creds.name,
image: undefined,
emailVerified: null,
});
return user
? adapter.updateUser({
id: user.id,
name: creds.name,
superAdmin: creds.superAdmin,
})
: adapter.createUser({
name: creds.name,
email: creds.name,
image: undefined,
emailVerified: null,
superAdmin: false,
});
},
}),
],
Expand Down
1 change: 1 addition & 0 deletions next/src/types/next-auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ declare module "next-auth" {

interface User {
image?: string;
superAdmin: boolean;
}
}
29 changes: 29 additions & 0 deletions next/src/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import clsx from "clsx";
import type { ButtonHTMLAttributes, Dispatch, SetStateAction } from "react";

export interface Props extends ButtonHTMLAttributes<HTMLInputElement> {
label?: string;
description?: string;
model: [boolean, Dispatch<SetStateAction<boolean>>];
className?: string;
}

function Checkbox(props: Props) {
return (
<div className={clsx("relative flex items-start", props.className)}>
<div className="flex h-6 items-center">
<input
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
checked={props.model[0]}
onChange={(e) => props.model[1](e.target.checked)}
/>
</div>
<div className="ml-3 text-sm leading-6 text-gray-400">
{props.label && <label className="font-medium">{props.label}</label>}
</div>
</div>
);
}

export default Checkbox;