mirror of
https://github.com/fosrl/pangolin.git
synced 2025-12-18 14:05:33 +00:00
add idp auto provision override on user
This commit is contained in:
@@ -454,6 +454,8 @@
|
|||||||
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
||||||
"userSaved": "User saved",
|
"userSaved": "User saved",
|
||||||
"userSavedDescription": "The user has been updated.",
|
"userSavedDescription": "The user has been updated.",
|
||||||
|
"autoProvisioned": "Auto Provisioned",
|
||||||
|
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
||||||
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
||||||
"accessControlsSubmit": "Save Access Controls",
|
"accessControlsSubmit": "Save Access Controls",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
@@ -911,6 +913,8 @@
|
|||||||
"idpConnectingToFinished": "Connected",
|
"idpConnectingToFinished": "Connected",
|
||||||
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
|
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
|
||||||
"idpErrorNotFound": "IdP not found",
|
"idpErrorNotFound": "IdP not found",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Invalid Invite",
|
"inviteInvalid": "Invalid Invite",
|
||||||
"inviteInvalidDescription": "The invite link is invalid.",
|
"inviteInvalidDescription": "The invite link is invalid.",
|
||||||
"inviteErrorWrongUser": "Invite is not for this user",
|
"inviteErrorWrongUser": "Invite is not for this user",
|
||||||
@@ -982,6 +986,8 @@
|
|||||||
"licenseTierProfessionalRequired": "Professional Edition Required",
|
"licenseTierProfessionalRequired": "Professional Edition Required",
|
||||||
"licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.",
|
"licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.",
|
||||||
"actionGetOrg": "Get Organization",
|
"actionGetOrg": "Get Organization",
|
||||||
|
"updateOrgUser": "Update Org User",
|
||||||
|
"createOrgUser": "Create Org User",
|
||||||
"actionUpdateOrg": "Update Organization",
|
"actionUpdateOrg": "Update Organization",
|
||||||
"actionUpdateUser": "Update User",
|
"actionUpdateUser": "Update User",
|
||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Get User",
|
||||||
@@ -1496,5 +1502,7 @@
|
|||||||
"convertButton": "Convert This Node to Managed Self-Hosted"
|
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "International Domain Detected",
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
"willbestoredas": "Will be stored as:"
|
"willbestoredas": "Will be stored as:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/idp/azure.png
Normal file
BIN
public/idp/azure.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
public/idp/google.png
Normal file
BIN
public/idp/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -100,7 +100,8 @@ export enum ActionsEnum {
|
|||||||
getApiKey = "getApiKey",
|
getApiKey = "getApiKey",
|
||||||
createOrgDomain = "createOrgDomain",
|
createOrgDomain = "createOrgDomain",
|
||||||
deleteOrgDomain = "deleteOrgDomain",
|
deleteOrgDomain = "deleteOrgDomain",
|
||||||
restartOrgDomain = "restartOrgDomain"
|
restartOrgDomain = "restartOrgDomain",
|
||||||
|
updateOrgUser = "updateOrgUser"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|||||||
@@ -213,7 +213,8 @@ export const userOrgs = pgTable("userOrgs", {
|
|||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId),
|
.references(() => roles.roleId),
|
||||||
isOwner: boolean("isOwner").notNull().default(false)
|
isOwner: boolean("isOwner").notNull().default(false),
|
||||||
|
autoProvisioned: boolean("autoProvisioned").default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const resources = sqliteTable("resources", {
|
|||||||
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
@@ -143,8 +143,11 @@ export const exitNodes = sqliteTable("exitNodes", {
|
|||||||
type: text("type").default("gerbil") // gerbil, remoteExitNode
|
type: text("type").default("gerbil") // gerbil, remoteExitNode
|
||||||
});
|
});
|
||||||
|
|
||||||
export const siteResources = sqliteTable("siteResources", { // this is for the clients
|
export const siteResources = sqliteTable("siteResources", {
|
||||||
siteResourceId: integer("siteResourceId").primaryKey({ autoIncrement: true }),
|
// this is for the clients
|
||||||
|
siteResourceId: integer("siteResourceId").primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
@@ -156,7 +159,7 @@ export const siteResources = sqliteTable("siteResources", { // this is for the c
|
|||||||
proxyPort: integer("proxyPort").notNull(),
|
proxyPort: integer("proxyPort").notNull(),
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
destinationPort: integer("destinationPort").notNull(),
|
||||||
destinationIp: text("destinationIp").notNull(),
|
destinationIp: text("destinationIp").notNull(),
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = sqliteTable("user", {
|
export const users = sqliteTable("user", {
|
||||||
@@ -260,7 +263,9 @@ export const clientSites = sqliteTable("clientSites", {
|
|||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false),
|
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
endpoint: text("endpoint")
|
endpoint: text("endpoint")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,7 +323,10 @@ export const userOrgs = sqliteTable("userOrgs", {
|
|||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId),
|
.references(() => roles.roleId),
|
||||||
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false)
|
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
|
||||||
|
autoProvisioned: integer("autoProvisioned", {
|
||||||
|
mode: "boolean"
|
||||||
|
}).default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
||||||
|
|||||||
@@ -582,6 +582,14 @@ authenticated.put(
|
|||||||
user.createOrgUser
|
user.createOrgUser
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/user/:userId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateOrgUser),
|
||||||
|
user.updateOrgUser
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
|
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, idpOidcConfig } from "@server/db";
|
||||||
import { domains, idp, orgDomains, users, idpOrg } from "@server/db";
|
import { domains, idp, orgDomains, users, idpOrg } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
@@ -33,10 +33,13 @@ async function query(limit: number, offset: number) {
|
|||||||
idpId: idp.idpId,
|
idpId: idp.idpId,
|
||||||
name: idp.name,
|
name: idp.name,
|
||||||
type: idp.type,
|
type: idp.type,
|
||||||
orgCount: sql<number>`count(${idpOrg.orgId})`
|
variant: idpOidcConfig.variant,
|
||||||
|
orgCount: sql<number>`count(${idpOrg.orgId})`,
|
||||||
|
autoProvision: idp.autoProvision
|
||||||
})
|
})
|
||||||
.from(idp)
|
.from(idp)
|
||||||
.leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`)
|
.leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`)
|
||||||
|
.leftJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId))
|
||||||
.groupBy(idp.idpId)
|
.groupBy(idp.idpId)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
@@ -44,12 +47,7 @@ async function query(limit: number, offset: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ListIdpsResponse = {
|
export type ListIdpsResponse = {
|
||||||
idps: Array<{
|
idps: Awaited<ReturnType<typeof query>>;
|
||||||
idpId: number;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
orgCount: number;
|
|
||||||
}>;
|
|
||||||
pagination: {
|
pagination: {
|
||||||
total: number;
|
total: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
|||||||
@@ -354,8 +354,13 @@ export async function validateOidcCallback(
|
|||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(eq(userOrgs.userId, userId!));
|
.where(eq(userOrgs.userId, userId!));
|
||||||
|
|
||||||
// Delete orgs that are no longer valid
|
// Filter to only auto-provisioned orgs for CRUD operations
|
||||||
const orgsToDelete = currentUserOrgs.filter(
|
const autoProvisionedOrgs = currentUserOrgs.filter(
|
||||||
|
(org) => org.autoProvisioned === true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete auto-provisioned orgs that are no longer valid
|
||||||
|
const orgsToDelete = autoProvisionedOrgs.filter(
|
||||||
(currentOrg) =>
|
(currentOrg) =>
|
||||||
!userOrgInfo.some(
|
!userOrgInfo.some(
|
||||||
(newOrg) => newOrg.orgId === currentOrg.orgId
|
(newOrg) => newOrg.orgId === currentOrg.orgId
|
||||||
@@ -374,8 +379,8 @@ export async function validateOidcCallback(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update roles for existing orgs where the role has changed
|
// Update roles for existing auto-provisioned orgs where the role has changed
|
||||||
const orgsToUpdate = currentUserOrgs.filter((currentOrg) => {
|
const orgsToUpdate = autoProvisionedOrgs.filter((currentOrg) => {
|
||||||
const newOrg = userOrgInfo.find(
|
const newOrg = userOrgInfo.find(
|
||||||
(newOrg) => newOrg.orgId === currentOrg.orgId
|
(newOrg) => newOrg.orgId === currentOrg.orgId
|
||||||
);
|
);
|
||||||
@@ -401,7 +406,7 @@ export async function validateOidcCallback(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new orgs that don't exist yet
|
// Add new orgs that don't exist yet (these will be auto-provisioned)
|
||||||
const orgsToAdd = userOrgInfo.filter(
|
const orgsToAdd = userOrgInfo.filter(
|
||||||
(newOrg) =>
|
(newOrg) =>
|
||||||
!currentUserOrgs.some(
|
!currentUserOrgs.some(
|
||||||
@@ -415,12 +420,14 @@ export async function validateOidcCallback(
|
|||||||
userId: userId!,
|
userId: userId!,
|
||||||
orgId: org.orgId,
|
orgId: org.orgId,
|
||||||
roleId: org.roleId,
|
roleId: org.roleId,
|
||||||
|
autoProvisioned: true,
|
||||||
dateCreated: new Date().toISOString()
|
dateCreated: new Date().toISOString()
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all the orgs and get the total number of users from the userOrgs table
|
// Loop through all the orgs and get the total number of users from the userOrgs table
|
||||||
|
// Use all current user orgs (both auto-provisioned and manually added) for counting
|
||||||
for (const org of currentUserOrgs) {
|
for (const org of currentUserOrgs) {
|
||||||
const userCount = await trx
|
const userCount = await trx
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import {
|
|||||||
verifyApiKeyIsRoot,
|
verifyApiKeyIsRoot,
|
||||||
verifyApiKeyClientAccess,
|
verifyApiKeyClientAccess,
|
||||||
verifyClientsEnabled,
|
verifyClientsEnabled,
|
||||||
verifyApiKeySiteResourceAccess
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyOrgAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
@@ -469,6 +470,21 @@ authenticated.get(
|
|||||||
user.listUsers
|
user.listUsers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/user",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createOrgUser),
|
||||||
|
user.createOrgUser
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/user/:userId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyUserAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateOrgUser),
|
||||||
|
user.updateOrgUser
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/org/:orgId/user/:userId",
|
"/org/:orgId/user/:userId",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
@@ -84,7 +84,14 @@ export async function createOrgUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const { username, email, name, type, idpId, roleId } = parsedBody.data;
|
const {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
idpId,
|
||||||
|
roleId
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
const [role] = await db
|
const [role] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -173,7 +180,8 @@ export async function createOrgUser(
|
|||||||
.values({
|
.values({
|
||||||
orgId,
|
orgId,
|
||||||
userId: existingUser.userId,
|
userId: existingUser.userId,
|
||||||
roleId: role.roleId
|
roleId: role.roleId,
|
||||||
|
autoProvisioned: false
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
} else {
|
} else {
|
||||||
@@ -189,7 +197,7 @@ export async function createOrgUser(
|
|||||||
type: "oidc",
|
type: "oidc",
|
||||||
idpId,
|
idpId,
|
||||||
dateCreated: new Date().toISOString(),
|
dateCreated: new Date().toISOString(),
|
||||||
emailVerified: true
|
emailVerified: true,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -198,7 +206,8 @@ export async function createOrgUser(
|
|||||||
.values({
|
.values({
|
||||||
orgId,
|
orgId,
|
||||||
userId: newUser.userId,
|
userId: newUser.userId,
|
||||||
roleId: role.roleId
|
roleId: role.roleId,
|
||||||
|
autoProvisioned: false
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
}
|
}
|
||||||
@@ -209,7 +218,6 @@ export async function createOrgUser(
|
|||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(eq(userOrgs.orgId, orgId));
|
.where(eq(userOrgs.orgId, orgId));
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, idp, idpOidcConfig } from "@server/db";
|
||||||
import { roles, userOrgs, users } from "@server/db";
|
import { roles, userOrgs, users } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -25,10 +25,18 @@ async function queryUser(orgId: string, userId: string) {
|
|||||||
isOwner: userOrgs.isOwner,
|
isOwner: userOrgs.isOwner,
|
||||||
isAdmin: roles.isAdmin,
|
isAdmin: roles.isAdmin,
|
||||||
twoFactorEnabled: users.twoFactorEnabled,
|
twoFactorEnabled: users.twoFactorEnabled,
|
||||||
|
autoProvisioned: userOrgs.autoProvisioned,
|
||||||
|
idpId: users.idpId,
|
||||||
|
idpName: idp.name,
|
||||||
|
idpType: idp.type,
|
||||||
|
idpVariant: idpOidcConfig.variant,
|
||||||
|
idpAutoProvision: idp.autoProvision
|
||||||
})
|
})
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
||||||
.leftJoin(users, eq(userOrgs.userId, users.userId))
|
.leftJoin(users, eq(userOrgs.userId, users.userId))
|
||||||
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
.leftJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId))
|
||||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export * from "./removeInvitation";
|
|||||||
export * from "./createOrgUser";
|
export * from "./createOrgUser";
|
||||||
export * from "./adminUpdateUser2FA";
|
export * from "./adminUpdateUser2FA";
|
||||||
export * from "./adminGetUser";
|
export * from "./adminGetUser";
|
||||||
|
export * from "./updateOrgUser";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, idpOidcConfig } from "@server/db";
|
||||||
import { idp, roles, userOrgs, users } from "@server/db";
|
import { idp, roles, userOrgs, users } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -50,12 +50,15 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
|
|||||||
isOwner: userOrgs.isOwner,
|
isOwner: userOrgs.isOwner,
|
||||||
idpName: idp.name,
|
idpName: idp.name,
|
||||||
idpId: users.idpId,
|
idpId: users.idpId,
|
||||||
|
idpType: idp.type,
|
||||||
|
idpVariant: idpOidcConfig.variant,
|
||||||
twoFactorEnabled: users.twoFactorEnabled,
|
twoFactorEnabled: users.twoFactorEnabled,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.leftJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
.leftJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
||||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId))
|
||||||
.where(eq(userOrgs.orgId, orgId))
|
.where(eq(userOrgs.orgId, orgId))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|||||||
112
server/routers/user/updateOrgUser.ts
Normal file
112
server/routers/user/updateOrgUser.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, userOrgs } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const paramsSchema = z
|
||||||
|
.object({
|
||||||
|
userId: z.string(),
|
||||||
|
orgId: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const bodySchema = z
|
||||||
|
.object({
|
||||||
|
autoProvisioned: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
message: "At least one field must be provided for update"
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/org/{orgId}/user/{userId}",
|
||||||
|
description: "Update a user in an org.",
|
||||||
|
tags: [OpenAPITags.Org, OpenAPITags.User],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function updateOrgUser(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [existingUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"User not found in this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = parsedBody.data;
|
||||||
|
|
||||||
|
const [updatedUser] = await db
|
||||||
|
.update(userOrgs)
|
||||||
|
.set({
|
||||||
|
...updateData
|
||||||
|
})
|
||||||
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: updatedUser,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Org user updated successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { InviteUserResponse } from "@server/routers/user";
|
import { InviteUserResponse } from "@server/routers/user";
|
||||||
@@ -41,6 +42,8 @@ import { formatAxiosError } from "@app/lib/api";
|
|||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
|
||||||
export default function AccessControlsPage() {
|
export default function AccessControlsPage() {
|
||||||
const { orgUser: user } = userOrgUserContext();
|
const { orgUser: user } = userOrgUserContext();
|
||||||
@@ -56,14 +59,16 @@ export default function AccessControlsPage() {
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
|
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') }),
|
||||||
|
autoProvisioned: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: user.username!,
|
username: user.username!,
|
||||||
roleId: user.roleId?.toString()
|
roleId: user.roleId?.toString(),
|
||||||
|
autoProvisioned: user.autoProvisioned || false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,31 +96,36 @@ export default function AccessControlsPage() {
|
|||||||
fetchRoles();
|
fetchRoles();
|
||||||
|
|
||||||
form.setValue("roleId", user.roleId.toString());
|
form.setValue("roleId", user.roleId.toString());
|
||||||
|
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const res = await api
|
try {
|
||||||
.post<
|
// Execute both API calls simultaneously
|
||||||
AxiosResponse<InviteUserResponse>
|
const [roleRes, userRes] = await Promise.all([
|
||||||
>(`/role/${values.roleId}/add/${user.userId}`)
|
api.post<AxiosResponse<InviteUserResponse>>(`/role/${values.roleId}/add/${user.userId}`),
|
||||||
.catch((e) => {
|
api.post(`/org/${orgId}/user/${user.userId}`, {
|
||||||
toast({
|
autoProvisioned: values.autoProvisioned
|
||||||
variant: "destructive",
|
})
|
||||||
title: t('accessRoleErrorAdd'),
|
]);
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t('accessRoleErrorAddDescription')
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.status === 200) {
|
if (roleRes.status === 200 && userRes.status === 200) {
|
||||||
|
toast({
|
||||||
|
variant: "default",
|
||||||
|
title: t('userSaved'),
|
||||||
|
description: t('userSavedDescription')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "destructive",
|
||||||
title: t('userSaved'),
|
title: t('accessRoleErrorAdd'),
|
||||||
description: t('userSavedDescription')
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t('accessRoleErrorAddDescription')
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +150,20 @@ export default function AccessControlsPage() {
|
|||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="access-controls-form"
|
id="access-controls-form"
|
||||||
>
|
>
|
||||||
|
{/* IDP Type Display */}
|
||||||
|
{user.type !== UserType.Internal && user.idpType && (
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t("idp")}:
|
||||||
|
</span>
|
||||||
|
<IdpTypeBadge
|
||||||
|
type={user.idpType}
|
||||||
|
variant={user.idpVariant || undefined}
|
||||||
|
name={user.idpName || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="roleId"
|
name="roleId"
|
||||||
@@ -147,7 +171,13 @@ export default function AccessControlsPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('role')}</FormLabel>
|
<FormLabel>{t('role')}</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
// If auto provision is enabled, set it to false when role changes
|
||||||
|
if (user.idpAutoProvision) {
|
||||||
|
form.setValue("autoProvisioned", false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -170,6 +200,31 @@ export default function AccessControlsPage() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{user.idpAutoProvision && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="autoProvisioned"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>
|
||||||
|
{t('autoProvisioned')}
|
||||||
|
</FormLabel>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t('autoProvisionedDescription')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { Checkbox } from "@app/components/ui/checkbox";
|
|||||||
import { ListIdpsResponse } from "@server/routers/idp";
|
import { ListIdpsResponse } from "@server/routers/idp";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
type UserType = "internal" | "oidc";
|
type UserType = "internal" | "oidc";
|
||||||
|
|
||||||
@@ -53,6 +54,17 @@ interface IdpOption {
|
|||||||
idpId: number;
|
idpId: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
variant: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserOption {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
disabled: boolean;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
idpId?: number;
|
||||||
|
variant?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -62,14 +74,14 @@ export default function Page() {
|
|||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const [userType, setUserType] = useState<UserType | null>("internal");
|
const [selectedOption, setSelectedOption] = useState<string | null>("internal");
|
||||||
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [expiresInDays, setExpiresInDays] = useState(1);
|
const [expiresInDays, setExpiresInDays] = useState(1);
|
||||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||||
const [idps, setIdps] = useState<IdpOption[]>([]);
|
const [idps, setIdps] = useState<IdpOption[]>([]);
|
||||||
const [sendEmail, setSendEmail] = useState(env.email.emailEnabled);
|
const [sendEmail, setSendEmail] = useState(env.email.emailEnabled);
|
||||||
const [selectedIdp, setSelectedIdp] = useState<IdpOption | null>(null);
|
const [userOptions, setUserOptions] = useState<UserOption[]>([]);
|
||||||
const [dataLoaded, setDataLoaded] = useState(false);
|
const [dataLoaded, setDataLoaded] = useState(false);
|
||||||
|
|
||||||
const internalFormSchema = z.object({
|
const internalFormSchema = z.object({
|
||||||
@@ -80,7 +92,13 @@ export default function Page() {
|
|||||||
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalFormSchema = z.object({
|
const googleAzureFormSchema = z.object({
|
||||||
|
email: z.string().email({ message: t("emailInvalid") }),
|
||||||
|
name: z.string().optional(),
|
||||||
|
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
||||||
|
});
|
||||||
|
|
||||||
|
const genericOidcFormSchema = z.object({
|
||||||
username: z.string().min(1, { message: t("usernameRequired") }),
|
username: z.string().min(1, { message: t("usernameRequired") }),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
@@ -96,11 +114,44 @@ export default function Page() {
|
|||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case "oidc":
|
case "oidc":
|
||||||
return t("idpGenericOidc");
|
return t("idpGenericOidc");
|
||||||
|
case "google":
|
||||||
|
return t("idpGoogleDescription");
|
||||||
|
case "azure":
|
||||||
|
return t("idpAzureDescription");
|
||||||
default:
|
default:
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getIdpIcon = (variant: string | null) => {
|
||||||
|
if (!variant) return null;
|
||||||
|
|
||||||
|
switch (variant.toLowerCase()) {
|
||||||
|
case "google":
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src="/idp/google.png"
|
||||||
|
alt={t("idpGoogleAlt")}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "azure":
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src="/idp/azure.png"
|
||||||
|
alt={t("idpAzureAlt")}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validFor = [
|
const validFor = [
|
||||||
{ hours: 24, name: t("day", { count: 1 }) },
|
{ hours: 24, name: t("day", { count: 1 }) },
|
||||||
{ hours: 48, name: t("day", { count: 2 }) },
|
{ hours: 48, name: t("day", { count: 2 }) },
|
||||||
@@ -120,8 +171,17 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalForm = useForm<z.infer<typeof externalFormSchema>>({
|
const googleAzureForm = useForm<z.infer<typeof googleAzureFormSchema>>({
|
||||||
resolver: zodResolver(externalFormSchema),
|
resolver: zodResolver(googleAzureFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
name: "",
|
||||||
|
roleId: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const genericOidcForm = useForm<z.infer<typeof genericOidcFormSchema>>({
|
||||||
|
resolver: zodResolver(genericOidcFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
@@ -132,33 +192,19 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userType === "internal") {
|
if (selectedOption === "internal") {
|
||||||
setSendEmail(env.email.emailEnabled);
|
setSendEmail(env.email.emailEnabled);
|
||||||
internalForm.reset();
|
internalForm.reset();
|
||||||
setInviteLink(null);
|
setInviteLink(null);
|
||||||
setExpiresInDays(1);
|
setExpiresInDays(1);
|
||||||
} else if (userType === "oidc") {
|
} else if (selectedOption && selectedOption !== "internal") {
|
||||||
externalForm.reset();
|
googleAzureForm.reset();
|
||||||
|
genericOidcForm.reset();
|
||||||
}
|
}
|
||||||
}, [userType, env.email.emailEnabled, internalForm, externalForm]);
|
}, [selectedOption, env.email.emailEnabled, internalForm, googleAzureForm, genericOidcForm]);
|
||||||
|
|
||||||
const [userTypes, setUserTypes] = useState<StrategyOption<string>[]>([
|
|
||||||
{
|
|
||||||
id: "internal",
|
|
||||||
title: t("userTypeInternal"),
|
|
||||||
description: t("userTypeInternalDescription"),
|
|
||||||
disabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "oidc",
|
|
||||||
title: t("userTypeExternal"),
|
|
||||||
description: t("userTypeExternalDescription"),
|
|
||||||
disabled: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userType) {
|
if (!selectedOption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,20 +245,6 @@ export default function Page() {
|
|||||||
|
|
||||||
if (res?.status === 200) {
|
if (res?.status === 200) {
|
||||||
setIdps(res.data.data.idps);
|
setIdps(res.data.data.idps);
|
||||||
|
|
||||||
if (res.data.data.idps.length) {
|
|
||||||
setUserTypes((prev) =>
|
|
||||||
prev.map((type) => {
|
|
||||||
if (type.id === "oidc") {
|
|
||||||
return {
|
|
||||||
...type,
|
|
||||||
disabled: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return type;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +258,33 @@ export default function Page() {
|
|||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Build user options when IDPs are loaded
|
||||||
|
useEffect(() => {
|
||||||
|
const options: UserOption[] = [
|
||||||
|
{
|
||||||
|
id: "internal",
|
||||||
|
title: t("userTypeInternal"),
|
||||||
|
description: t("userTypeInternalDescription"),
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add IDP options
|
||||||
|
idps.forEach((idp) => {
|
||||||
|
options.push({
|
||||||
|
id: `idp-${idp.idpId}`,
|
||||||
|
title: idp.name,
|
||||||
|
description: formatIdpType(idp.variant || idp.type),
|
||||||
|
disabled: false,
|
||||||
|
icon: getIdpIcon(idp.variant),
|
||||||
|
idpId: idp.idpId,
|
||||||
|
variant: idp.variant
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setUserOptions(options);
|
||||||
|
}, [idps, t]);
|
||||||
|
|
||||||
async function onSubmitInternal(
|
async function onSubmitInternal(
|
||||||
values: z.infer<typeof internalFormSchema>
|
values: z.infer<typeof internalFormSchema>
|
||||||
) {
|
) {
|
||||||
@@ -274,9 +333,52 @@ export default function Page() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitExternal(
|
async function onSubmitGoogleAzure(
|
||||||
values: z.infer<typeof externalFormSchema>
|
values: z.infer<typeof googleAzureFormSchema>
|
||||||
) {
|
) {
|
||||||
|
const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
|
||||||
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const res = await api
|
||||||
|
.put(`/org/${orgId}/user`, {
|
||||||
|
username: values.email, // Use email as username for Google/Azure
|
||||||
|
email: values.email,
|
||||||
|
name: values.name,
|
||||||
|
type: "oidc",
|
||||||
|
idpId: selectedUserOption.idpId,
|
||||||
|
roleId: parseInt(values.roleId)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("userErrorCreate"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("userErrorCreateDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 201) {
|
||||||
|
toast({
|
||||||
|
variant: "default",
|
||||||
|
title: t("userCreated"),
|
||||||
|
description: t("userCreatedDescription")
|
||||||
|
});
|
||||||
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmitGenericOidc(
|
||||||
|
values: z.infer<typeof genericOidcFormSchema>
|
||||||
|
) {
|
||||||
|
const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
|
||||||
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -285,7 +387,7 @@ export default function Page() {
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
idpId: parseInt(values.idpId),
|
idpId: selectedUserOption.idpId,
|
||||||
roleId: parseInt(values.roleId)
|
roleId: parseInt(values.roleId)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -330,7 +432,7 @@ export default function Page() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{!inviteLink && build !== "saas" ? (
|
{!inviteLink && build !== "saas" && dataLoaded ? (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
@@ -342,15 +444,15 @@ export default function Page() {
|
|||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<StrategySelect
|
<StrategySelect
|
||||||
options={userTypes}
|
options={userOptions}
|
||||||
defaultValue={userType || undefined}
|
defaultValue={selectedOption || undefined}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setUserType(value as UserType);
|
setSelectedOption(value);
|
||||||
if (value === "internal") {
|
if (value === "internal") {
|
||||||
internalForm.reset();
|
internalForm.reset();
|
||||||
} else if (value === "oidc") {
|
} else {
|
||||||
externalForm.reset();
|
googleAzureForm.reset();
|
||||||
setSelectedIdp(null);
|
genericOidcForm.reset();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
cols={2}
|
cols={2}
|
||||||
@@ -359,7 +461,7 @@ export default function Page() {
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{userType === "internal" && dataLoaded && (
|
{selectedOption === "internal" && dataLoaded && (
|
||||||
<>
|
<>
|
||||||
{!inviteLink ? (
|
{!inviteLink ? (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
@@ -564,71 +666,7 @@ export default function Page() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userType !== "internal" && dataLoaded && (
|
{selectedOption && selectedOption !== "internal" && dataLoaded && (
|
||||||
<>
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{t("idpTitle")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
{t("idpSelect")}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
{idps.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
{t("idpNotConfigured")}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<Form {...externalForm}>
|
|
||||||
<FormField
|
|
||||||
control={externalForm.control}
|
|
||||||
name="idpId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<StrategySelect
|
|
||||||
options={idps.map(
|
|
||||||
(idp) => ({
|
|
||||||
id: idp.idpId.toString(),
|
|
||||||
title: idp.name,
|
|
||||||
description:
|
|
||||||
formatIdpType(
|
|
||||||
idp.type
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
defaultValue={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
onChange={(
|
|
||||||
value
|
|
||||||
) => {
|
|
||||||
field.onChange(
|
|
||||||
value
|
|
||||||
);
|
|
||||||
const idp =
|
|
||||||
idps.find(
|
|
||||||
(idp) =>
|
|
||||||
idp.idpId.toString() ===
|
|
||||||
value
|
|
||||||
);
|
|
||||||
setSelectedIdp(
|
|
||||||
idp || null
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
cols={2}
|
|
||||||
/>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
{idps.length > 0 && (
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
@@ -640,144 +678,206 @@ export default function Page() {
|
|||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...externalForm}>
|
{/* Google/Azure Form */}
|
||||||
<form
|
{(() => {
|
||||||
onSubmit={externalForm.handleSubmit(
|
const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
|
||||||
onSubmitExternal
|
return selectedUserOption?.variant === "google" || selectedUserOption?.variant === "azure";
|
||||||
)}
|
})() && (
|
||||||
className="space-y-4"
|
<Form {...googleAzureForm}>
|
||||||
id="create-user-form"
|
<form
|
||||||
>
|
onSubmit={googleAzureForm.handleSubmit(
|
||||||
<FormField
|
onSubmitGoogleAzure
|
||||||
control={
|
|
||||||
externalForm.control
|
|
||||||
}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"username"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{t(
|
|
||||||
"usernameUniq"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
className="space-y-4"
|
||||||
|
id="create-user-form"
|
||||||
<FormField
|
>
|
||||||
control={
|
<FormField
|
||||||
externalForm.control
|
control={googleAzureForm.control}
|
||||||
}
|
name="email"
|
||||||
name="email"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>
|
||||||
<FormLabel>
|
{t("email")}
|
||||||
{t(
|
</FormLabel>
|
||||||
"emailOptional"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={
|
|
||||||
externalForm.control
|
|
||||||
}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"nameOptional"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={
|
|
||||||
externalForm.control
|
|
||||||
}
|
|
||||||
name="roleId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("role")}
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-full">
|
<Input
|
||||||
<SelectValue
|
{...field}
|
||||||
placeholder={t(
|
/>
|
||||||
"accessRoleSelect"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<FormMessage />
|
||||||
{roles.map(
|
</FormItem>
|
||||||
(
|
)}
|
||||||
role
|
/>
|
||||||
) => (
|
|
||||||
|
<FormField
|
||||||
|
control={googleAzureForm.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("nameOptional")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={googleAzureForm.control}
|
||||||
|
name="roleId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("role")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t("accessRoleSelect")}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{roles.map((role) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={
|
key={role.roleId}
|
||||||
role.roleId
|
|
||||||
}
|
|
||||||
value={role.roleId.toString()}
|
value={role.roleId.toString()}
|
||||||
>
|
>
|
||||||
{
|
{role.name}
|
||||||
role.name
|
|
||||||
}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)
|
))}
|
||||||
)}
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Generic OIDC Form */}
|
||||||
|
{(() => {
|
||||||
|
const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
|
||||||
|
return selectedUserOption?.variant !== "google" && selectedUserOption?.variant !== "azure";
|
||||||
|
})() && (
|
||||||
|
<Form {...genericOidcForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={genericOidcForm.handleSubmit(
|
||||||
|
onSubmitGenericOidc
|
||||||
)}
|
)}
|
||||||
/>
|
className="space-y-4"
|
||||||
</form>
|
id="create-user-form"
|
||||||
</Form>
|
>
|
||||||
|
<FormField
|
||||||
|
control={genericOidcForm.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("username")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{t("usernameUniq")}
|
||||||
|
</p>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={genericOidcForm.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("emailOptional")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={genericOidcForm.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("nameOptional")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={genericOidcForm.control}
|
||||||
|
name="roleId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("role")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t("accessRoleSelect")}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{roles.map((role) => (
|
||||||
|
<SelectItem
|
||||||
|
key={role.roleId}
|
||||||
|
value={role.roleId.toString()}
|
||||||
|
>
|
||||||
|
{role.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 mt-8">
|
<div className="flex justify-end space-x-2 mt-8">
|
||||||
{userType && dataLoaded && (
|
{selectedOption && dataLoaded && (
|
||||||
<Button
|
<Button
|
||||||
type={inviteLink ? "button" : "submit"}
|
type={inviteLink ? "button" : "submit"}
|
||||||
form={inviteLink ? undefined : "create-user-form"}
|
form={inviteLink ? undefined : "create-user-form"}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default async function UsersPage(props: UsersPageProps) {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
type: user.type,
|
type: user.type,
|
||||||
|
idpVariant: user.idpVariant,
|
||||||
idpId: user.idpId,
|
idpId: user.idpId,
|
||||||
idpName: user.idpName || t('idpNameInternal'),
|
idpName: user.idpName || t('idpNameInternal'),
|
||||||
status: t('userConfirmed'),
|
status: t('userConfirmed'),
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export default async function Page(props: {
|
|||||||
)();
|
)();
|
||||||
const loginIdps = idpsRes.data.data.idps.map((idp) => ({
|
const loginIdps = idpsRes.data.data.idps.map((idp) => ({
|
||||||
idpId: idp.idpId,
|
idpId: idp.idpId,
|
||||||
name: idp.name
|
name: idp.name,
|
||||||
|
variant: idp.variant
|
||||||
})) as LoginFormIDP[];
|
})) as LoginFormIDP[];
|
||||||
|
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ import {
|
|||||||
} from "@app/components/ui/dropdown-menu";
|
} from "@app/components/ui/dropdown-menu";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import IdpTypeBadge from "./IdpTypeBadge";
|
||||||
|
|
||||||
export type IdpRow = {
|
export type IdpRow = {
|
||||||
idpId: number;
|
idpId: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
orgCount: number;
|
orgCount: number;
|
||||||
|
variant?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -57,15 +59,6 @@ export default function IdpTable({ idps }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeDisplay = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case "oidc":
|
|
||||||
return "OAuth2/OIDC";
|
|
||||||
default:
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: ColumnDef<IdpRow>[] = [
|
const columns: ColumnDef<IdpRow>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "idpId",
|
accessorKey: "idpId",
|
||||||
@@ -116,9 +109,8 @@ export default function IdpTable({ idps }: Props) {
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const type = row.original.type;
|
const type = row.original.type;
|
||||||
return (
|
const variant = row.original.variant;
|
||||||
<Badge variant="secondary">{getTypeDisplay(type)}</Badge>
|
return <IdpTypeBadge type={type} variant={variant} />;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
62
src/components/IdpTypeBadge.tsx
Normal file
62
src/components/IdpTypeBadge.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
type IdpTypeBadgeProps = {
|
||||||
|
type: string;
|
||||||
|
variant?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function IdpTypeBadge({
|
||||||
|
type,
|
||||||
|
variant,
|
||||||
|
name
|
||||||
|
}: IdpTypeBadgeProps) {
|
||||||
|
const effectiveType = variant || type;
|
||||||
|
const effectiveName = name || formatType(effectiveType);
|
||||||
|
|
||||||
|
function formatType(type: string) {
|
||||||
|
if (type === "google") return "Google";
|
||||||
|
if (type === "azure") return "Azure";
|
||||||
|
if (type === "oidc") return "OAuth2/OIDC";
|
||||||
|
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="inline-flex items-center space-x-1 w-fit"
|
||||||
|
>
|
||||||
|
{effectiveType === "google" && (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
src="/idp/google.png"
|
||||||
|
alt="Google"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span>{effectiveName}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{effectiveType === "azure" && (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
src="/idp/azure.png"
|
||||||
|
alt="Azure"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span>{effectiveName}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{effectiveType === "oidc" && <span>{effectiveName}</span>}
|
||||||
|
{!["google", "azure", "oidc"].includes(effectiveType) && (
|
||||||
|
<span>{effectiveName}</span>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ import { startAuthentication } from "@simplewebauthn/browser";
|
|||||||
export type LoginFormIDP = {
|
export type LoginFormIDP = {
|
||||||
idpId: number;
|
idpId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
variant?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoginFormProps = {
|
type LoginFormProps = {
|
||||||
@@ -496,19 +497,41 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{idps.map((idp) => (
|
{idps.map((idp) => {
|
||||||
<Button
|
const effectiveType = idp.variant || idp.name.toLowerCase();
|
||||||
key={idp.idpId}
|
|
||||||
type="button"
|
return (
|
||||||
variant="outline"
|
<Button
|
||||||
className="w-full"
|
key={idp.idpId}
|
||||||
onClick={() => {
|
type="button"
|
||||||
loginWithIdp(idp.idpId);
|
variant="outline"
|
||||||
}}
|
className="w-full inline-flex items-center space-x-2"
|
||||||
>
|
onClick={() => {
|
||||||
{idp.name}
|
loginWithIdp(idp.idpId);
|
||||||
</Button>
|
}}
|
||||||
))}
|
>
|
||||||
|
{effectiveType === "google" && (
|
||||||
|
<Image
|
||||||
|
src="/idp/google.png"
|
||||||
|
alt="Google"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{effectiveType === "azure" && (
|
||||||
|
<Image
|
||||||
|
src="/idp/azure.png"
|
||||||
|
alt="Azure"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{idp.name}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t('actionListInvitations')]: "listInvitations",
|
[t('actionListInvitations')]: "listInvitations",
|
||||||
[t('actionRemoveUser')]: "removeUser",
|
[t('actionRemoveUser')]: "removeUser",
|
||||||
[t('actionListUsers')]: "listUsers",
|
[t('actionListUsers')]: "listUsers",
|
||||||
[t('actionListOrgDomains')]: "listOrgDomains"
|
[t('actionListOrgDomains')]: "listOrgDomains",
|
||||||
|
[t('updateOrgUser')]: "updateOrgUser",
|
||||||
|
[t('createOrgUser')]: "createOrgUser"
|
||||||
},
|
},
|
||||||
|
|
||||||
Site: {
|
Site: {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { createApiClient } from "@app/lib/api";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import IdpTypeBadge from "./IdpTypeBadge";
|
||||||
|
|
||||||
export type UserRow = {
|
export type UserRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,6 +32,7 @@ export type UserRow = {
|
|||||||
idpId: number | null;
|
idpId: number | null;
|
||||||
idpName: string;
|
idpName: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
idpVariant: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
role: string;
|
role: string;
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
@@ -81,6 +83,16 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRow = row.original;
|
||||||
|
return (
|
||||||
|
<IdpTypeBadge
|
||||||
|
type={userRow.type}
|
||||||
|
name={userRow.idpName}
|
||||||
|
variant={userRow.idpVariant || undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user