Skip to content

Commit

Permalink
Creating backend functions for user management (#322)
Browse files Browse the repository at this point in the history
Co-authored-by: Katie Campbell Downie <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Katie Campbell Downie <[email protected]>
  • Loading branch information
4 people authored Feb 4, 2025
1 parent 68129de commit e069eea
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 25 deletions.
2 changes: 1 addition & 1 deletion query-connector/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ services:

# Next.js app with Flyway
query-connector:
platform: linux/amd64
build:
context: .
dockerfile: Dockerfile
Expand All @@ -41,6 +40,7 @@ services:
- AUTH_DISABLED=false
- AUTH_SECRET="ido5D/uybeAB3AmMQwn+ubw2zYC4t2h7RJlW2R79598="
- AUTH_KEYCLOAK_ISSUER=http://localhost:8080/realms/master
- NEXTAUTH_URL=http://localhost:3000
- LOCAL_KEYCLOAK=http://localhost:8080
- NAMED_KEYCLOAK=http://keycloak:8080
- AUTH_KEYCLOAK_ID=query-connector
Expand Down
20 changes: 15 additions & 5 deletions query-connector/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion query-connector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "1.0.1",
"private": true,
"scripts": {
"dev": "docker compose -f docker-compose-dev.yaml up -d && next dev",
"dev": "docker compose -f docker-compose-dev.yaml up -d && next dev; docker compose -f docker-compose-dev.yaml down --remove-orphans",
"dev-win": "start docker compose -f docker-compose-dev.yaml up && next dev",
"dev:db": "docker compose -f docker-compose-dev.yaml up",
"dev:next": "dotenv -e ./.env -- next dev",
Expand Down
14 changes: 14 additions & 0 deletions query-connector/src/@types/next-auth.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import NextAuth from "next-auth";
declare module "next-auth" {
interface User {
id: string;
username?: string | null;
firstName?: string | null;
lastName?: string | null;
}

interface Session {
user: User;
}
}
export default NextAuth;
67 changes: 67 additions & 0 deletions query-connector/src/app/backend/user-management.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use server";

import { getDbClient } from "./dbClient";
const dbClient = getDbClient();

/**
* Adds a user to the user_management table if they do not already exist.
* Uses data extracted from the JWT token.
* @param userToken - The user data from the JWT token.
* @param userToken.id - The user ID from the JWT token.
* @param userToken.username - The username from the JWT token.
* @param userToken.email - The email from the JWT token.
* @param userToken.firstName - The first name from the JWT token.
* @param userToken.lastName - The last name from the JWT token.
* @returns The newly added user or an empty result if already exists.
*/
export async function addUserIfNotExists(userToken: {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
}) {
if (!userToken || !userToken.username) {
console.error("Invalid user token. Cannot add user.");
return;
}

const { id, username, email, firstName, lastName } = userToken;
const userIdentifier = username || email;

try {
console.log("Checking if user exists:", id);

const checkUserQuery = `SELECT username FROM user_management WHERE username = $1;`;
const userExists = await dbClient.query(checkUserQuery, [userIdentifier]);

if (userExists.rows.length > 0) {
console.log("User already exists in user_management:", id);
return userExists.rows[0];
}

console.log("User not found. Proceeding to insert:", id);

// TODO: Update the role based on the user's group in Keycloak
const qc_role = "super-admin";

const insertUserQuery = `
INSERT INTO user_management (username, qc_role, first_name, last_name)
VALUES ($1, $2, $3, $4)
RETURNING username, qc_role, first_name, last_name;
`;

const result = await dbClient.query(insertUserQuery, [
userIdentifier,
qc_role,
firstName,
lastName,
]);

console.log("User added to user_management", id);
return result.rows[0];
} catch (error) {
console.error("Error adding user to user_management:", error);
throw error;
}
}
6 changes: 3 additions & 3 deletions query-connector/src/app/ui/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React, { useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
import { useSession, signIn } from "next-auth/react";
import { Icon } from "@trussworks/react-uswds";
import { Button, Icon } from "@trussworks/react-uswds";
import styles from "./header.module.scss";
import { metadata } from "@/app/shared/constants";
import classNames from "classnames";
Expand Down Expand Up @@ -91,7 +91,7 @@ const HeaderComponent: React.FC<{ authDisabled: boolean }> = ({
)}
>
{/* TODO: Enable this once we can show/hide rules based on actual auth status */}
{/* {!isLoggedIn && !LOGGED_IN_PATHS.includes(path) && (
{!isLoggedIn && !LOGGED_IN_PATHS.includes(path) && (
<Button
className={styles.signinButton}
type="button"
Expand All @@ -101,7 +101,7 @@ const HeaderComponent: React.FC<{ authDisabled: boolean }> = ({
>
Sign in
</Button>
)} */}
)}
{LOGGED_IN_PATHS.includes(path) && (
<button
onClick={toggleMenuDropdown}
Expand Down
34 changes: 19 additions & 15 deletions query-connector/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import NextAuth from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";
import { addUserIfNotExists } from "@/app/backend/user-management";

export const { handlers, signIn, signOut, auth } = NextAuth({
secret: process.env.AUTH_SECRET,
Expand Down Expand Up @@ -31,20 +32,26 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
* @returns The updated JWT token with user details.
*/
async jwt({ token, profile }) {
console.log("JWT Callback - Initial Token:", token);
console.log("JWT Callback - Profile:", profile);

if (profile) {
token.id = profile.sub;
token.username = profile.preferred_username || profile.email;
token.email = profile.email;
token.firstName = profile.given_name;
token.lastName = profile.family_name;
const userToken = {
id: profile.sub || "",
username: profile.preferred_username || profile.email || "",
email: profile.email || "",
firstName: profile.given_name || "",
lastName: profile.family_name || "",
};

// Ensure user is in the database **only on first login**
try {
await addUserIfNotExists(userToken);
} catch (error) {}

return { ...token, ...userToken };
}

console.log("JWT Callback - Final Token:", token);
return token;
},

/**
* Session callback to pass user data to the session object.
* @param session The root object containing session properties.
Expand All @@ -53,17 +60,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
* @returns The updated session object with user details.
*/
async session({ session, token }) {
console.log("Session Callback - Initial Session:", session);
console.log("Session Callback - Token:", token);

session.user = {
...token,
id: typeof token.id === "string" ? token.id : "",
email: token.email || "",
username: typeof token.username === "string" ? token.username : "",
firstName: typeof token.firstName === "string" ? token.firstName : "",
lastName: typeof token.lastName === "string" ? token.lastName : "",
emailVerified: null,
};

console.log("Session Callback - Final Session:", session);
return session;
},
},
Expand Down

0 comments on commit e069eea

Please sign in to comment.