From 59d9ba88a41a2012d801ecf0a1b8624ea3c99e4b Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 1 May 2025 18:43:09 +0800 Subject: [PATCH 001/143] addition of new options --- .../CippComponents/CippFormComponent.jsx | 3 + .../resources/management/list-rooms/edit.jsx | 554 ++++++++++++++---- 2 files changed, 458 insertions(+), 99 deletions(-) diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index f42f5b1fbb16..b0f3e571656f 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -155,6 +155,9 @@ export const CippFormComponent = (props) => { { + const timezones = `Azores Standard Time (UTC-01:00) Azores +Cape Verde Standard Time (UTC-01:00) Cabo Verde Is. +UTC-02 (UTC-02:00) Co-ordinated Universal Time-02 +Greenland Standard Time (UTC-02:00) Greenland +Mid-Atlantic Standard Time (UTC-02:00) Mid-Atlantic - Old +Tocantins Standard Time (UTC-03:00) Araguaina +Paraguay Standard Time (UTC-03:00) Asuncion +E. South America Standard Time (UTC-03:00) Brasilia +SA Eastern Standard Time (UTC-03:00) Cayenne, Fortaleza +Argentina Standard Time (UTC-03:00) City of Buenos Aires +Montevideo Standard Time (UTC-03:00) Montevideo +Magallanes Standard Time (UTC-03:00) Punta Arenas +Saint Pierre Standard Time (UTC-03:00) Saint Pierre and Miquelon +Bahia Standard Time (UTC-03:00) Salvador +Newfoundland Standard Time (UTC-03:30) Newfoundland +Atlantic Standard Time (UTC-04:00) Atlantic Time (Canada) +Venezuela Standard Time (UTC-04:00) Caracas +Central Brazilian Standard Time (UTC-04:00) Cuiaba +SA Western Standard Time (UTC-04:00) Georgetown, La Paz, Manaus, San Juan +Pacific SA Standard Time (UTC-04:00) Santiago +SA Pacific Standard Time (UTC-05:00) Bogota, Lima, Quito, Rio Branco +Eastern Standard Time (Mexico) (UTC-05:00) Chetumal +Eastern Standard Time (UTC-05:00) Eastern Time (US & Canada) +Haiti Standard Time (UTC-05:00) Haiti +Cuba Standard Time (UTC-05:00) Havana +US Eastern Standard Time (UTC-05:00) Indiana (East) +Turks And Caicos Standard Time (UTC-05:00) Turks and Caicos +Central America Standard Time (UTC-06:00) Central America +Central Standard Time (UTC-06:00) Central Time (US & Canada) +Easter Island Standard Time (UTC-06:00) Easter Island +Central Standard Time (Mexico) (UTC-06:00) Guadalajara, Mexico City, Monterrey +Canada Central Standard Time (UTC-06:00) Saskatchewan +US Mountain Standard Time (UTC-07:00) Arizona +Mountain Standard Time (Mexico) (UTC-07:00) La Paz, Mazatlan +Mountain Standard Time (UTC-07:00) Mountain Time (US & Canada) +Yukon Standard Time (UTC-07:00) Yukon +Pacific Standard Time (Mexico) (UTC-08:00) Baja California +UTC-08 (UTC-08:00) Co-ordinated Universal Time-08 +Pacific Standard Time (UTC-08:00) Pacific Time (US & Canada) +Alaskan Standard Time (UTC-09:00) Alaska +UTC-09 (UTC-09:00) Co-ordinated Universal Time-09 +Marquesas Standard Time (UTC-09:30) Marquesas Islands +Aleutian Standard Time (UTC-10:00) Aleutian Islands +Hawaiian Standard Time (UTC-10:00) Hawaii +UTC-11 (UTC-11:00) Co-ordinated Universal Time-11 +Dateline Standard Time (UTC-12:00) International Date Line West +UTC (UTC) Co-ordinated Universal Time +GMT Standard Time (UTC+00:00) Dublin, Edinburgh, Lisbon, London +Greenwich Standard Time (UTC+00:00) Monrovia, Reykjavik +Sao Tome Standard Time (UTC+00:00) Sao Tome +W. Europe Standard Time (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna +Central Europe Standard Time (UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague +Romance Standard Time (UTC+01:00) Brussels, Copenhagen, Madrid, Paris +Morocco Standard Time (UTC+01:00) Casablanca +Central European Standard Time (UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb +W. Central Africa Standard Time (UTC+01:00) West Central Africa +GTB Standard Time (UTC+02:00) Athens, Bucharest +Middle East Standard Time (UTC+02:00) Beirut +Egypt Standard Time (UTC+02:00) Cairo +E. Europe Standard Time (UTC+02:00) Chisinau +West Bank Standard Time (UTC+02:00) Gaza, Hebron +South Africa Standard Time (UTC+02:00) Harare, Pretoria +FLE Standard Time (UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius +Israel Standard Time (UTC+02:00) Jerusalem +South Sudan Standard Time (UTC+02:00) Juba +Kaliningrad Standard Time (UTC+02:00) Kaliningrad +Sudan Standard Time (UTC+02:00) Khartoum +Libya Standard Time (UTC+02:00) Tripoli +Namibia Standard Time (UTC+02:00) Windhoek +Jordan Standard Time (UTC+03:00) Amman +Arabic Standard Time (UTC+03:00) Baghdad +Syria Standard Time (UTC+03:00) Damascus +Turkey Standard Time (UTC+03:00) Istanbul +Arab Standard Time (UTC+03:00) Kuwait, Riyadh +Belarus Standard Time (UTC+03:00) Minsk +Russian Standard Time (UTC+03:00) Moscow, St Petersburg +E. Africa Standard Time (UTC+03:00) Nairobi +Volgograd Standard Time (UTC+03:00) Volgograd +Iran Standard Time (UTC+03:30) Tehran +Arabian Standard Time (UTC+04:00) Abu Dhabi, Muscat +Astrakhan Standard Time (UTC+04:00) Astrakhan, Ulyanovsk +Azerbaijan Standard Time (UTC+04:00) Baku +Russia Time Zone 3 (UTC+04:00) Izhevsk, Samara +Mauritius Standard Time (UTC+04:00) Port Louis +Saratov Standard Time (UTC+04:00) Saratov +Georgian Standard Time (UTC+04:00) Tbilisi +Caucasus Standard Time (UTC+04:00) Yerevan +Afghanistan Standard Time (UTC+04:30) Kabul +West Asia Standard Time (UTC+05:00) Ashgabat, Tashkent +Qyzylorda Standard Time (UTC+05:00) Astana +Ekaterinburg Standard Time (UTC+05:00) Ekaterinburg +Pakistan Standard Time (UTC+05:00) Islamabad, Karachi +India Standard Time (UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi +Sri Lanka Standard Time (UTC+05:30) Sri Jayawardenepura +Nepal Standard Time (UTC+05:45) Kathmandu +Central Asia Standard Time (UTC+06:00) Bishkek +Bangladesh Standard Time (UTC+06:00) Dhaka +Omsk Standard Time (UTC+06:00) Omsk +Myanmar Standard Time (UTC+06:30) Yangon (Rangoon) +SE Asia Standard Time (UTC+07:00) Bangkok, Hanoi, Jakarta +Altai Standard Time (UTC+07:00) Barnaul, Gorno-Altaysk +W. Mongolia Standard Time (UTC+07:00) Hovd +North Asia Standard Time (UTC+07:00) Krasnoyarsk +N. Central Asia Standard Time (UTC+07:00) Novosibirsk +Tomsk Standard Time (UTC+07:00) Tomsk +China Standard Time (UTC+08:00) Beijing, Chongqing, Hong Kong SAR, Urumqi +North Asia East Standard Time (UTC+08:00) Irkutsk +Singapore Standard Time (UTC+08:00) Kuala Lumpur, Singapore +W. Australia Standard Time (UTC+08:00) Perth +Taipei Standard Time (UTC+08:00) Taipei +Ulaanbaatar Standard Time (UTC+08:00) Ulaanbaatar +Aus Central W. Standard Time (UTC+08:45) Eucla +Transbaikal Standard Time (UTC+09:00) Chita +Tokyo Standard Time (UTC+09:00) Osaka, Sapporo, Tokyo +North Korea Standard Time (UTC+09:00) Pyongyang +Korea Standard Time (UTC+09:00) Seoul +Yakutsk Standard Time (UTC+09:00) Yakutsk +Cen. Australia Standard Time (UTC+09:30) Adelaide +AUS Central Standard Time (UTC+09:30) Darwin +E. Australia Standard Time (UTC+10:00) Brisbane +AUS Eastern Standard Time (UTC+10:00) Canberra, Melbourne, Sydney +West Pacific Standard Time (UTC+10:00) Guam, Port Moresby +Tasmania Standard Time (UTC+10:00) Hobart +Vladivostok Standard Time (UTC+10:00) Vladivostok +Lord Howe Standard Time (UTC+10:30) Lord Howe Island +Bougainville Standard Time (UTC+11:00) Bougainville Island +Russia Time Zone 10 (UTC+11:00) Chokurdakh +Magadan Standard Time (UTC+11:00) Magadan +Norfolk Standard Time (UTC+11:00) Norfolk Island +Sakhalin Standard Time (UTC+11:00) Sakhalin +Central Pacific Standard Time (UTC+11:00) Solomon Is., New Caledonia +Russia Time Zone 11 (UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky +New Zealand Standard Time (UTC+12:00) Auckland, Wellington +UTC+12 (UTC+12:00) Co-ordinated Universal Time+12 +Fiji Standard Time (UTC+12:00) Fiji +Kamchatka Standard Time (UTC+12:00) Petropavlovsk-Kamchatsky - Old +Chatham Islands Standard Time (UTC+12:45) Chatham Islands +UTC+13 (UTC+13:00) Co-ordinated Universal Time+13 +Tonga Standard Time (UTC+13:00) Nuku'alofa +Samoa Standard Time (UTC+13:00) Samoa +Line Islands Standard Time (UTC+14:00) Kiritimati Island`; + + return timezones.split('\n').map(line => { + const parts = line.trim().split(/\s{2,}/); + if (parts.length >= 2) { + return { + value: parts[0].trim(), + label: parts[1].trim(), + }; + } + return null; + }).filter(Boolean); +}; + +// Work days options - just using the actual days +const workDaysOptions = [ + { value: "Sunday", label: "Sunday" }, + { value: "Monday", label: "Monday" }, + { value: "Tuesday", label: "Tuesday" }, + { value: "Wednesday", label: "Wednesday" }, + { value: "Thursday", label: "Thursday" }, + { value: "Friday", label: "Friday" }, + { value: "Saturday", label: "Saturday" } +]; + +// Automation Processing Options +const automateProcessingOptions = [ + { value: "None", label: "None - No processing" }, + { value: "AutoUpdate", label: "AutoUpdate - Accept/Decline but not delete" }, + { value: "AutoAccept", label: "AutoAccept - Accept and delete" } +]; + const EditRoomMailbox = () => { const router = useRouter(); const { roomId } = router.query; @@ -55,6 +229,29 @@ const EditRoomMailbox = () => { isWheelChairAccessible: room.isWheelChairAccessible, phone: room.phone, tags: room.tags?.map(tag => ({ label: tag, value: tag })) || [], + + // Calendar Properties + AllowConflicts: room.AllowConflicts, + AllowRecurringMeetings: room.AllowRecurringMeetings, + BookingWindowInDays: room.BookingWindowInDays, + MaximumDurationInMinutes: room.MaximumDurationInMinutes, + ProcessExternalMeetingMessages: room.ProcessExternalMeetingMessages, + EnforceCapacity: room.EnforceCapacity, + ForwardRequestsToDelegates: room.ForwardRequestsToDelegates, + ScheduleOnlyDuringWorkHours: room.ScheduleOnlyDuringWorkHours, + AutomateProcessing: room.AutomateProcessing, + + // Calendar Configuration + WorkDays: room.WorkDays?.split(',')?.map(day => ({ + label: day.trim(), + value: day.trim() + })) || [], + WorkHoursStartTime: room.WorkHoursStartTime, + WorkHoursEndTime: room.WorkHoursEndTime, + WorkingHoursTimeZone: room.WorkingHoursTimeZone ? { + value: room.WorkingHoursTimeZone, + label: createTimezoneOptions().find(tz => tz.value === room.WorkingHoursTimeZone)?.label || room.WorkingHoursTimeZone + } : null }); } }, [roomInfo.isSuccess, roomInfo.data]); @@ -101,10 +298,31 @@ const EditRoomMailbox = () => { isWheelChairAccessible: values.isWheelChairAccessible, phone: values.phone?.trim(), tags: values.tags?.map(tag => tag.value), + + // Calendar Properties + AllowConflicts: values.AllowConflicts, + AllowRecurringMeetings: values.AllowRecurringMeetings, + BookingWindowInDays: values.BookingWindowInDays, + MaximumDurationInMinutes: values.MaximumDurationInMinutes, + ProcessExternalMeetingMessages: values.ProcessExternalMeetingMessages, + EnforceCapacity: values.EnforceCapacity, + ForwardRequestsToDelegates: values.ForwardRequestsToDelegates, + ScheduleOnlyDuringWorkHours: values.ScheduleOnlyDuringWorkHours, + AutomateProcessing: values.AutomateProcessing?.value || values.AutomateProcessing, + + // Calendar Configuration + WorkDays: values.WorkDays?.map(day => day.value).join(','), + WorkHoursStartTime: values.WorkHoursStartTime, + WorkHoursEndTime: values.WorkHoursEndTime, + WorkingHoursTimeZone: values.WorkingHoursTimeZone?.value || values.WorkingHoursTimeZone, })} > - {/* Core & Booking Settings */} + {/* Basic Information */} + + Basic Information + + { + + + + + + {/* Booking Settings */} + + Booking Settings + + + { /> - + + + + + + + + + + + + + - + + + + + + + + + + + + + - {/* Location Information */} + {/* Working Hours */} - Location Information + Working Hours - {/* Building and Floor Info */} - + - - - - - - - - - - - - {/* Address Fields */} - + - - {/* City and Postal Code */} - - - - - - - - - {/* State and Country */} - - - - - - ({ - label: Name, - value: Code, - }))} - formControl={formControl} - /> - - + + + + + + + + + + - {/* Room Equipment */} + {/* Room Facilities */} - Room Equipment + Room Facilities & Equipment + + + + + + + + @@ -275,30 +562,99 @@ const EditRoomMailbox = () => { /> + + + + - {/* Room Features */} + {/* Location Information */} - Room Features + Location Information - + + + + + + + + + + + + + + + + + + + + + + + + + ({ + label: Name, + value: Code, + }))} formControl={formControl} - multiple={true} - creatable={true} /> @@ -308,4 +664,4 @@ const EditRoomMailbox = () => { EditRoomMailbox.getLayout = (page) => {page}; -export default EditRoomMailbox; \ No newline at end of file +export default EditRoomMailbox; \ No newline at end of file From dbdd972d2ea18edcb3a46d3d36425da8817463c5 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 3 May 2025 00:14:58 +0800 Subject: [PATCH 002/143] Addition of new languages --- src/data/languageList.json | 266 ++++++++++++++++++++++++++++++++++++- 1 file changed, 265 insertions(+), 1 deletion(-) diff --git a/src/data/languageList.json b/src/data/languageList.json index fea7ddddc8ca..c4e742af3a1e 100644 --- a/src/data/languageList.json +++ b/src/data/languageList.json @@ -5,6 +5,96 @@ "tag": "ar-SA", "LCID": "1025" }, + { + "language": "Arabic", + "Geographic area": "Algeria", + "tag": "ar-DZ", + "LCID": "5121" + }, + { + "language": "Arabic", + "Geographic area": "Egypt", + "tag": "ar-EG", + "LCID": "3073" + }, + { + "language": "Arabic", + "Geographic area": "Bahrain", + "tag": "ar-BH", + "LCID": "15361" + }, + { + "language": "Arabic", + "Geographic area": "Iraq", + "tag": "ar-IQ", + "LCID": "2049" + }, + { + "language": "Arabic", + "Geographic area": "Jordan", + "tag": "ar-JO", + "LCID": "11265" + }, + { + "language": "Arabic", + "Geographic area": "Kuwait", + "tag": "ar-KW", + "LCID": "13313" + }, + { + "language": "Arabic", + "Geographic area": "Lebanon", + "tag": "ar-LB", + "LCID": "12289" + }, + { + "language": "Arabic", + "Geographic area": "Libya", + "tag": "ar-LY", + "LCID": "4097" + }, + { + "language": "Arabic", + "Geographic area": "Morocco", + "tag": "ar-MA", + "LCID": "6145" + }, + { + "language": "Arabic", + "Geographic area": "Oman", + "tag": "ar-OM", + "LCID": "8193" + }, + { + "language": "Arabic", + "Geographic area": "Qatar", + "tag": "ar-QA", + "LCID": "16385" + }, + { + "language": "Arabic", + "Geographic area": "Syria", + "tag": "ar-SY", + "LCID": "10241" + }, + { + "language": "Arabic", + "Geographic area": "Tunisia", + "tag": "ar-TN", + "LCID": "7169" + }, + { + "language": "Arabic", + "Geographic area": "UAE", + "tag": "ar-AE", + "LCID": "14337" + }, + { + "language": "Arabic", + "Geographic area": "Yemen", + "tag": "ar-YE", + "LCID": "9217" + }, { "language": "Bulgarian", "Geographic area": "Bulgaria", @@ -23,6 +113,12 @@ "tag": "zh-TW", "LCID": "1028" }, + { + "language": "Chinese", + "Geographic area": "Hong Kong SAR", + "tag": "zh-HK", + "LCID": "3076" + }, { "language": "Croatian", "Geographic area": "Croatia", @@ -53,6 +149,42 @@ "tag": "en-US", "LCID": "1033" }, + { + "language": "English", + "Geographic area": "Australia", + "tag": "en-AU", + "LCID": "3081" + }, + { + "language": "English", + "Geographic area": "United Kingdom", + "tag": "en-GB", + "LCID": "2057" + }, + { + "language": "English", + "Geographic area": "New Zealand", + "tag": "en-NZ", + "LCID": "5129" + }, + { + "language": "English", + "Geographic area": "Canada", + "tag": "en-CA", + "LCID": "4105" + }, + { + "language": "English", + "Geographic area": "South Africa", + "tag": "en-ZA", + "LCID": "7177" + }, + { + "language": "English", + "Geographic area": "Singapore", + "tag": "en-SG", + "LCID": "4100" + }, { "language": "Estonian", "Geographic area": "Estonia", @@ -71,12 +203,30 @@ "tag": "fr-FR", "LCID": "1036" }, + { + "language": "French", + "Geographic area": "Canada", + "tag": "fr-CA", + "LCID": "3084" + }, + { + "language": "French", + "Geographic area": "Switzerland", + "tag": "fr-CH", + "LCID": "4108" + }, { "language": "German", "Geographic area": "Germany", "tag": "de-DE", "LCID": "1031" }, + { + "language": "German", + "Geographic area": "Switzerland", + "tag": "de-CH", + "LCID": "2055" + }, { "language": "Greek", "Geographic area": "Greece", @@ -155,6 +305,12 @@ "tag": "nb-NO", "LCID": "1044" }, + { + "language": "Persian", + "Geographic area": "Iran", + "tag": "fa-IR", + "LCID": "1065" + }, { "language": "Polish", "Geographic area": "Poland", @@ -209,6 +365,108 @@ "tag": "es-ES", "LCID": "3082" }, + { + "language": "Spanish", + "Geographic area": "Argentina", + "tag": "es-AR", + "LCID": "11274" + }, + { + "language": "Spanish", + "Geographic area": "Bolivia", + "tag": "es-BO", + "LCID": "16394" + }, + { + "language": "Spanish", + "Geographic area": "Chile", + "tag": "es-CL", + "LCID": "13322" + }, + { + "language": "Spanish", + "Geographic area": "Colombia", + "tag": "es-CO", + "LCID": "9226" + }, + { + "language": "Spanish", + "Geographic area": "Costa Rica", + "tag": "es-CR", + "LCID": "5130" + }, + { + "language": "Spanish", + "Geographic area": "Dominican Republic", + "tag": "es-DO", + "LCID": "7178" + }, + { + "language": "Spanish", + "Geographic area": "Ecuador", + "tag": "es-EC", + "LCID": "12298" + }, + { + "language": "Spanish", + "Geographic area": "El Salvador", + "tag": "es-SV", + "LCID": "17418" + }, + { + "language": "Spanish", + "Geographic area": "Guatemala", + "tag": "es-GT", + "LCID": "4106" + }, + { + "language": "Spanish", + "Geographic area": "Honduras", + "tag": "es-HN", + "LCID": "18442" + }, + { + "language": "Spanish", + "Geographic area": "Mexico", + "tag": "es-MX", + "LCID": "2058" + }, + { + "language": "Spanish", + "Geographic area": "Nicaragua", + "tag": "es-NI", + "LCID": "19466" + }, + { + "language": "Spanish", + "Geographic area": "Panama", + "tag": "es-PA", + "LCID": "6154" + }, + { + "language": "Spanish", + "Geographic area": "Paraguay", + "tag": "es-PY", + "LCID": "15370" + }, + { + "language": "Spanish", + "Geographic area": "Peru", + "tag": "es-PE", + "LCID": "10250" + }, + { + "language": "Spanish", + "Geographic area": "Uruguay", + "tag": "es-UY", + "LCID": "14346" + }, + { + "language": "Spanish", + "Geographic area": "Venezuela", + "tag": "es-VE", + "LCID": "8202" + }, { "language": "Swedish", "Geographic area": "Sweden", @@ -229,10 +487,16 @@ }, { "language": "Ukrainian", - "Geographic area": "Ukrainian", + "Geographic area": "Ukraine", "tag": "uk-UA", "LCID": "1058" }, + { + "language": "Urdu", + "Geographic area": "Pakistan", + "tag": "ur-PK", + "LCID": "1056" + }, { "language": "Vietnamese", "Geographic area": "Vietnam", From b89770ed383bcce28b5e5a09e843a6e61aed90a6 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 3 May 2025 13:13:57 +0800 Subject: [PATCH 003/143] parse raw alert data to populate the configurable input --- .../alert-configuration/alert.jsx | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index bae9c27ba61c..a07ad0d7e572 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -104,20 +104,37 @@ const AlertWizard = () => { alert.RawAlert.PostExecution.split(",").includes(opt.value) ); - // Reset the form with all values at once - formControl.reset( - { - tenantFilter: { - value: alert.RawAlert.Tenant, - label: alert.RawAlert.Tenant, - }, - excludedTenants: excludedTenantsFormatted, - command: { value: usedCommand, label: usedCommand.label }, - recurrence: recurrenceOption, - postExecution: postExecutionValue, + // Create the reset object with all the form values + const resetObject = { + tenantFilter: { + value: alert.RawAlert.Tenant, + label: alert.RawAlert.Tenant, }, - { keepDirty: false } - ); + excludedTenants: excludedTenantsFormatted, + command: { value: usedCommand, label: usedCommand.label }, + recurrence: recurrenceOption, + postExecution: postExecutionValue, + }; + + // Parse Parameters field if it exists and is a string + if (usedCommand?.requiresInput && alert.RawAlert.Parameters) { + try { + // Check if Parameters is a string that needs parsing + const params = typeof alert.RawAlert.Parameters === 'string' + ? JSON.parse(alert.RawAlert.Parameters) + : alert.RawAlert.Parameters; + + // Set the input value if it exists + if (params.InputValue) { + resetObject[usedCommand.inputName] = params.InputValue; + } + } catch (error) { + console.error("Error parsing parameters:", error); + } + } + + // Reset the form with all values at once + formControl.reset(resetObject, { keepDirty: false }); } if (alert?.PartitionKey === "Webhookv2") { setAlertType("audit"); From bdd1fe1e98187ea745ea1bd14f76456f96ea7c47 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 4 May 2025 20:01:26 +0800 Subject: [PATCH 004/143] Default filter for table, updated mailbox report --- .../CippComponents/CippTablePage.jsx | 10 ++++++++ src/components/CippTable/CippDataTable.js | 24 +++++++++++++++++++ .../SharedMailboxEnabledAccount/index.js | 20 ++++++++-------- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/components/CippComponents/CippTablePage.jsx b/src/components/CippComponents/CippTablePage.jsx index 37a3a9d44ccc..eb75f6cc1ad1 100644 --- a/src/components/CippComponents/CippTablePage.jsx +++ b/src/components/CippComponents/CippTablePage.jsx @@ -4,6 +4,7 @@ import Head from "next/head"; import { CippDataTable } from "../CippTable/CippDataTable"; import { useSettings } from "../../hooks/use-settings"; import { CippHead } from "./CippHead"; +import { useState } from "react"; export const CippTablePage = (props) => { const { @@ -23,10 +24,12 @@ export const CippTablePage = (props) => { queryKey, tableFilter, tenantInTitle = true, + filters, sx = { flexGrow: 1, py: 4 }, ...other } = props; const tenant = useSettings().currentTenant; + const [tableFilters] = useState(filters || []); return ( <> @@ -61,6 +64,13 @@ export const CippTablePage = (props) => { columns={columns} columnsFromApi={columnsFromApi} offCanvas={offCanvas} + filters={tableFilters} + initialState={{ + columnFilters: filters ? filters.map(filter => ({ + id: filter.id || filter.columnId, + value: filter.value + })) : [] + }} {...other} /> diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 058f97c09772..6c2269f0f6c1 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -66,6 +66,7 @@ export const CippDataTable = (props) => { const [actionData, setActionData] = useState({ data: {}, action: {}, ready: false }); const [graphFilterData, setGraphFilterData] = useState({}); const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); const waitingBool = api?.url ? true : false; const settings = useSettings(); @@ -78,6 +79,12 @@ export const CippDataTable = (props) => { ...graphFilterData, }); + useEffect(() => { + if (filters && Array.isArray(filters) && filters.length > 0) { + setColumnFilters(filters); + } + }, [filters]); + useEffect(() => { if (Array.isArray(data) && !api?.url) { if (!isEqual(data, usedData)) { @@ -208,6 +215,7 @@ export const CippDataTable = (props) => { state: { columnVisibility, sorting, + columnFilters, showSkeletons: getRequestData.isFetchingNextPage ? false : getRequestData.isFetching @@ -217,6 +225,7 @@ export const CippDataTable = (props) => { onSortingChange: (newSorting) => { setSorting(newSorting ?? []); }, + onColumnFiltersChange: setColumnFilters, renderEmptyRowsFallback: ({ table }) => getRequestData.data?.pages?.[0].Metadata?.QueueMessage ? ( @@ -435,6 +444,21 @@ export const CippDataTable = (props) => { }, }); + useEffect(() => { + if (filters && Array.isArray(filters) && filters.length > 0 && memoizedColumns.length > 0) { + // Make sure the table and columns are ready + setTimeout(() => { + if (table && typeof table.setColumnFilters === 'function') { + const formattedFilters = filters.map(filter => ({ + id: filter.id || filter.columnId, + value: filter.value + })); + table.setColumnFilters(formattedFilters); + } + },); + } + }, [filters, memoizedColumns, table]); + useEffect(() => { if (onChange && table.getSelectedRowModel().rows) { onChange(table.getSelectedRowModel().rows.map((row) => row.original)); diff --git a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js index 4b05494fa344..dbd167e86105 100644 --- a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js +++ b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js @@ -2,15 +2,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Block } from "@mui/icons-material"; -/* - NOTE for Devs: - - The original component used a Redux selector (`useSelector`) for tenant data, - which is handled by `CippTablePage` in the refactored version, thus eliminating `useSelector`. - - The `ModalService` with `confirm` handling was originally used to confirm blocking sign-in. - The action here replaces it with a confirmation text as per current guidelines. - - Original button and `FontAwesomeIcon` (faBan) are not used since action confirmation is handled by CippTablePage. -*/ - const Page = () => { return ( { icon: , url: "/api/ExecDisableUser", data: { ID: "id" }, - confirmText: "Are you sure you want to block the sign-in for this user?", + confirmText: "Are you sure you want to block the sign-in for this mailbox?", + condition: (row) => row.accountEnabled && !row.onPremisesSyncEnabled, }, ]} offCanvas={{ @@ -31,6 +23,7 @@ const Page = () => { "UserPrincipalName", "displayName", "accountEnabled", + "assignedLicenses", "onPremisesSyncEnabled", ], }} @@ -38,8 +31,15 @@ const Page = () => { "UserPrincipalName", "displayName", "accountEnabled", + "assignedLicenses", "onPremisesSyncEnabled", ]} + filters={[ + { + id: "accountEnabled", + value: "Yes" + } + ]} /> ); }; From 32f800a57e5597514b3115c4988bc83dc9a91531 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 5 May 2025 16:18:13 +0800 Subject: [PATCH 005/143] "recipientType" does not exist, conditional enable/disable device --- src/pages/identity/administration/devices/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/identity/administration/devices/index.js b/src/pages/identity/administration/devices/index.js index 09bd9ef0d42f..c547a0d800b1 100644 --- a/src/pages/identity/administration/devices/index.js +++ b/src/pages/identity/administration/devices/index.js @@ -27,6 +27,7 @@ const Page = () => { }, confirmText: "Are you sure you want to enable this device?", multiPost: false, + condition: (row) => !row.accountEnabled, icon: , }, { @@ -39,6 +40,7 @@ const Page = () => { }, confirmText: "Are you sure you want to disable this device?", multiPost: false, + condition: (row) => row.accountEnabled, icon: , }, { @@ -80,7 +82,7 @@ const Page = () => { simpleColumns={[ "displayName", "accountEnabled", - "recipientType", + "trustType", "enrollmentType", "manufacturer", "model", From 73e21827505106db36e94831953b78665d8d2745 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 5 May 2025 16:59:41 +0800 Subject: [PATCH 006/143] added all possible options to include WeekDay, WeekendDay and AllDays --- src/pages/email/resources/management/list-rooms/edit.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/email/resources/management/list-rooms/edit.jsx b/src/pages/email/resources/management/list-rooms/edit.jsx index edc4cfadc4f8..924c23734c32 100644 --- a/src/pages/email/resources/management/list-rooms/edit.jsx +++ b/src/pages/email/resources/management/list-rooms/edit.jsx @@ -165,7 +165,7 @@ Line Islands Standard Time (UTC+14:00) Kiritimati Island`; }).filter(Boolean); }; -// Work days options - just using the actual days +// Work days options const workDaysOptions = [ { value: "Sunday", label: "Sunday" }, { value: "Monday", label: "Monday" }, @@ -173,7 +173,10 @@ const workDaysOptions = [ { value: "Wednesday", label: "Wednesday" }, { value: "Thursday", label: "Thursday" }, { value: "Friday", label: "Friday" }, - { value: "Saturday", label: "Saturday" } + { value: "Saturday", label: "Saturday" }, + { value: "WeekDay", label: "Weekdays (Monday-Friday)" }, + { value: "WeekendDay", label: "Weekend (Saturday-Sunday)" }, + { value: "AllDays", label: "All Days" } ]; // Automation Processing Options From 72fab028e7dc708e595821597f83bd3c6aa21a76 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 5 May 2025 19:28:25 +0800 Subject: [PATCH 007/143] Fixes turning your computer into a heater at the un-authed screen --- src/pages/unauthenticated.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index c9999fa4c155..c2cb0d5d4352 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -3,7 +3,7 @@ import Head from "next/head"; import { CippImageCard } from "../components/CippCards/CippImageCard"; import { Layout as DashboardLayout } from "../layouts/index.js"; import { ApiGetCall } from "../api/ApiCall"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; const Page = () => { const orgData = ApiGetCall({ @@ -12,17 +12,18 @@ const Page = () => { staleTime: 120000, refetchOnWindowFocus: true, }); - const blockedRoles = ["anonymous", "authenticated"]; + const blockedRoles = useMemo(() => ["anonymous", "authenticated"], []); const [userRoles, setUserRoles] = useState([]); + const userRolesData = orgData.data?.clientPrincipal?.userRoles; useEffect(() => { - if (orgData.isSuccess) { - const roles = orgData.data?.clientPrincipal?.userRoles.filter( + if (orgData.isSuccess && userRolesData) { + const roles = userRolesData.filter( (role) => !blockedRoles.includes(role) ); setUserRoles(roles ?? []); } - }, [orgData, blockedRoles]); + }, [orgData.isSuccess, userRolesData, blockedRoles]); return ( <> From 98714bca956e2b4b27532f5523386e0fb9561b4f Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 6 May 2025 10:38:35 +0800 Subject: [PATCH 008/143] new remove after option, validation and helper text --- .../CippComponents/CippFormComponent.jsx | 5 + .../tenant-allow-block-lists/add.jsx | 165 +++++++++++++++++- 2 files changed, 167 insertions(+), 3 deletions(-) diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index f42f5b1fbb16..21e445d0a88d 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -189,6 +189,11 @@ export const CippFormComponent = (props) => { {get(errors, convertedName, {})?.message} + {other.helperText && ( + + {other.helperText} + + )} ); diff --git a/src/pages/email/administration/tenant-allow-block-lists/add.jsx b/src/pages/email/administration/tenant-allow-block-lists/add.jsx index 434e4cbaa7fa..d82b261ad03f 100644 --- a/src/pages/email/administration/tenant-allow-block-lists/add.jsx +++ b/src/pages/email/administration/tenant-allow-block-lists/add.jsx @@ -1,4 +1,5 @@ import React from "react"; +import { useEffect } from "react"; import { Grid } from "@mui/material"; import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; @@ -17,9 +18,56 @@ const AddTenantAllowBlockList = () => { listType: null, listMethod: null, NoExpiration: false, + RemoveAfter: false, }, }); + useEffect(() => { + const subscription = formControl.watch((value, { name }) => { + // If NoExpiration is turned on, disable RemoveAfter + if (name === "NoExpiration" && value.NoExpiration) { + formControl.setValue("RemoveAfter", false); + } + + // If RemoveAfter is turned on, disable NoExpiration + if (name === "RemoveAfter" && value.RemoveAfter) { + formControl.setValue("NoExpiration", false); + } + + // If ListMethod is Block, disable RemoveAfter + if (name === "listMethod" && value.listMethod?.value === "Block") { + formControl.setValue("RemoveAfter", false); + } + + // If listType is not Sender, Url, or FileHash, disable RemoveAfter + if (name === "listType" && !["Sender", "Url", "FileHash"].includes(value.listType?.value)) { + formControl.setValue("RemoveAfter", false); + } + + // If listType is FileHash, set ListMethod to Block + if (name === "listType" && value.listType?.value === "FileHash") { + formControl.setValue("listMethod", { label: "Block", value: "Block" }); + } + + // Handle NoExpiration compatibility based on rules + const listMethod = value.listMethod?.value; + const listType = value.listType?.value; + + // Check if NoExpiration should be enabled + const isNoExpirationCompatible = + listMethod === "Block" || + (listMethod === "Allow" && (listType === "Url" || listType === "IP")); + + // If current selection is incompatible with NoExpiration, reset it to false + if ((name === "listMethod" || name === "listType") && + !isNoExpirationCompatible && value.NoExpiration) { + formControl.setValue("NoExpiration", false); + } + }); + + return () => subscription.unsubscribe(); + }, [formControl]); + return ( { notes: values.notes, listMethod: values.listMethod?.value, NoExpiration: values.NoExpiration, + RemoveAfter: values.RemoveAfter }; }} > @@ -46,7 +95,78 @@ const AddTenantAllowBlockList = () => { label="Entries" name="entries" formControl={formControl} - validators={{ required: "Entries field is required" }} + validators={{ + required: "Entries field is required", + validate: (value) => { + if (!value) return true; + + const entries = value.split(/[,;]/).map(e => e.trim()); + const listType = formControl.watch("listType")?.value; + + if (listType === "FileHash") { + // SHA256 hash validation - 64 hex characters + for (const entry of entries) { + if (entry.length !== 64) + return "File hash entries must be exactly 64 characters"; + if (!/^[A-Fa-f0-9]{64}$/.test(entry)) + return "File hash must contain only hexadecimal characters"; + } + } else if (listType === "IP") { + // IPv6 address validation (for IP list type - IPv6 only) + const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/; + const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/; + + for (const entry of entries) { + if (!ipv6Regex.test(entry) && !ipv6CidrRegex.test(entry)) + return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation"; + } + } else if (listType === "Url") { + // For URL list type - check length and validate any IP addresses + // IPv4 regex + const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/; + // IPv4 CIDR regex + const ipv4CidrRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}\/([0-9]|[12][0-9]|3[0-2])$/; + // IPv6 regex (same as above) + const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/; + const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/; + + // Hostname regex (allowing wildcards * and ~) + const hostnameRegex = /^((\*|\~)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\*|\~)?$/; + + for (const entry of entries) { + if (entry.length > 250) + return "URL entries must be 250 characters or less"; + + // Skip validation for entries with wildcards (more complex patterns) + if (entry.includes('*') || entry.includes('~')) continue; + + // If it's not a hostname and not an IP, it might be invalid + if (!ipv4Regex.test(entry) && !ipv4CidrRegex.test(entry) && + !ipv6Regex.test(entry) && !ipv6CidrRegex.test(entry) && + !hostnameRegex.test(entry)) { + // Allow some flexibility for wildcard patterns + // But at least provide a warning for obviously invalid formats + if (!/[a-zA-Z0-9\.\-\*\~]/.test(entry)) { + return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses"; + } + } + } + } + + return true; + } + }} + helperText={ + formControl.watch("listType")?.value === "FileHash" + ? "Enter SHA256 hash values separated by commas or semicolons (e.g., 768a813668695ef2483b2bde7cf5d1b2db0423a0d3e63e498f3ab6f2eb13ea3)" + : formControl.watch("listType")?.value === "Url" + ? "Enter URLs, IPv4, or IPv6 addresses with optional wildcards separated by commas or semicolons" + : formControl.watch("listType")?.value === "Sender" + ? "Enter domains or email addresses separated by commas or semicolons (e.g., contoso.com,user@example.com)" + : formControl.watch("listType")?.value === "IP" + ? "Enter IPv6 addresses only in colon-hexadecimal format or CIDR notation" + : "" + } /> {/* Notes & List Type */} @@ -68,8 +188,9 @@ const AddTenantAllowBlockList = () => { creatable={false} options={[ { label: "Sender", value: "Sender" }, - { label: "Url", value: "Url" }, + { label: "Url/IPv4", value: "Url" }, { label: "FileHash", value: "FileHash" }, + { label: "IPv6", value: "IP" }, ]} validators={{ required: "Please choose a List Type." }} /> @@ -89,6 +210,16 @@ const AddTenantAllowBlockList = () => { { label: "Allow", value: "Allow" }, ]} validators={{ required: "Please select Block or Allow." }} + disabled={ + formControl.watch("listType")?.value === "FileHash" + ? true + : false + } + helperText={ + formControl.watch("listType")?.value === "FileHash" + ? "FileHash entries can only be Blocked" + : "Choose whether to block or allow the entries" + } /> @@ -96,9 +227,37 @@ const AddTenantAllowBlockList = () => { + + + {/* Remove After */} + + From 76502d96591766a67ea87875764170eb5d974101 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 6 May 2025 18:51:47 +0800 Subject: [PATCH 009/143] Less ambiguous wording for mailbox usage --- src/components/CippCards/CippExchangeInfoCard.jsx | 2 +- src/components/linearProgressWithLabel.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippCards/CippExchangeInfoCard.jsx b/src/components/CippCards/CippExchangeInfoCard.jsx index 03f4473c745a..1a368b99324d 100644 --- a/src/components/CippCards/CippExchangeInfoCard.jsx +++ b/src/components/CippCards/CippExchangeInfoCard.jsx @@ -82,7 +82,7 @@ export const CippExchangeInfoCard = (props) => { { - {`${Math.round(props.value)}% ${props?.addedLabel ?? ""}`} + {`${Math.round(props.value)}% ${props?.addedLabel ?? ""}`} ); }; From d6f3a2376f597799c13309730743c89537448321 Mon Sep 17 00:00:00 2001 From: Esco Date: Tue, 6 May 2025 13:25:14 +0200 Subject: [PATCH 010/143] chore: more cSpell words --- cspell.json | 73 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/cspell.json b/cspell.json index 921542d92e48..1ff07bf80063 100644 --- a/cspell.json +++ b/cspell.json @@ -4,28 +4,61 @@ "dictionaryDefinitions": [], "dictionaries": [], "words": [ - "CIPP", - "CIPP-API", - "Entra", - "Intune", - "GDAP", - "OBEE", - "AITM", - "Passwordless", - "Yubikey", - "Sherweb", - "Autotask", - "Datto", - "Syncro", - "ImmyBot", - "Choco", + "ADMS", + "AITM", + "Augmentt", + "Autotask", + "Choco", + "CIPP", + "CIPP-API", + "Datto", + "Entra", + "ESET", + "GDAP", + "HIBP", + "Hudu", + "ImmyBot", + "Intune", + "LCID", + "OBEE", + "Passwordless", + "pwpush", + "Rewst", + "Sherweb", + "Syncro", + "Yubikey" ], "ignoreWords": [ - "CIPPAPI", - "locationcipp", - "TNEF", - "winmail", - "PSTN", + "Addins", + "CIPPAPI", + "PSTN", + "TNEF", + "exo_individualsharing", + "exo_mailboxaudit", + "exo_mailtipsenabled", + "exo_outlookaddins", + "exo_storageproviderrestricted", + "locationcipp", + "mdo_antiphishingpolicies", + "mdo_autoforwardingmode", + "mdo_blockmailforward", + "mdo_commonattachmentsfilter", + "mdo_highconfidencephishaction", + "mdo_highconfidencespamaction", + "mdo_phishthresholdlevel", + "mdo_phisspamacation", + "mdo_safeattachmentpolicy", + "mdo_safeattachments", + "mdo_safedocuments", + "mdo_safelinksforOfficeApps", + "mdo_safelinksforemail", + "mdo_spam_notifications_only_for_admins", + "mdo_zapmalware", + "mdo_zapphish", + "mdo_zapspam", + "microsoftonline", + "mip_search_auditlog", + "winmail" ], "import": [] } From 1f267efa16ffd4b1a32825e42772d54e3341cec1 Mon Sep 17 00:00:00 2001 From: Esco Date: Wed, 7 May 2025 12:35:05 +0200 Subject: [PATCH 011/143] feat: Work from anywhere report Mainly for Windows 11 Readiness Report --- src/layouts/config.js | 1 + .../reports/workfromanywhere/index.js | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/pages/endpoint/reports/workfromanywhere/index.js diff --git a/src/layouts/config.js b/src/layouts/config.js index ccf67d86e9ba..b1d9e72b9623 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -258,6 +258,7 @@ export const nativeMenuItems = [ path: "/endpoint/reports", items: [ { title: "Analytics Device Score", path: "/endpoint/reports/analyticsdevicescore" }, + { title: "Work from anywhere", path: "/endpoint/reports/workfromanywhere" }, ], }, ], diff --git a/src/pages/endpoint/reports/workfromanywhere/index.js b/src/pages/endpoint/reports/workfromanywhere/index.js new file mode 100644 index 000000000000..cd963abfbe72 --- /dev/null +++ b/src/pages/endpoint/reports/workfromanywhere/index.js @@ -0,0 +1,87 @@ +import { EyeIcon } from "@heroicons/react/24/outline"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useSettings } from "/src/hooks/use-settings"; + +const Page = () => { + const pageTitle = "Work from anywhere Report"; + const tenantFilter = useSettings().currentTenant; + + // Actions from the source file + const actions = [ + { + label: "View in Intune", + link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, + color: "info", + icon: , + target: "_blank", + multiPost: false, + external: true, + }, + ]; + + // OffCanvas details based on the source file + const offCanvas = { + extendedInfoFields: [ + "id", + "deviceName", + "serialNumber", + "model", + "manufacturer", + "ownership", + "upgradeEligibility", + ], + actions: actions, + }; + + // Columns to be displayed in the table + const simpleColumns = [ + "deviceName", + "serialNumber", + "model", + "manufacturer", + "ownership", + "managedBy", + "osVersion", + "upgradeEligibility", + "ramCheckFailed", + "storageCheckFailed", + "processorCoreCountCheckFailed", + "processorSpeedCheckFailed", + "tpmCheckFailed", + "secureBootCheckFailed", + "processorFamilyCheckFailed", + "processor64BitCheckFailed", + "osCheckFailed", + ]; + + // Predefined filters to be applied to the table + const filterList = [ + { + filterName: "Upgrade not eligible", + value: [{ id: "upgradeEligibility", value: "notCapable" }], + type: "column", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 9c73651212296e4cd80d6929f610331900c1de63 Mon Sep 17 00:00:00 2001 From: Esco Date: Thu, 8 May 2025 09:59:15 +0200 Subject: [PATCH 012/143] fix: fix for disable Out of Office * fixes https://discord.com/channels/905453405936447518/905454401639047228/1369906134814818314 Fix disable state --- src/components/CippComponents/CippUserActions.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index b443b4bcb856..4b0f5fe1b6c9 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -154,7 +154,10 @@ export const CippUserActions = () => { type: "POST", icon: , url: "/api/ExecSetOoO", - data: { user: "userPrincipalName", AutoReplyState: "Disabled" }, + data: { + userId: "userPrincipalName", + AutoReplyState: { value: "Disabled" } + }, confirmText: "Are you sure you want to disable the out of office?", multiPost: false, }, From 5652ef9c196fc7cd33ee05f7d47a1edf851eb20a Mon Sep 17 00:00:00 2001 From: Esco Date: Fri, 9 May 2025 10:01:44 +0200 Subject: [PATCH 013/143] feat: Editable City and Country --- src/components/CippCards/CippUserInfoCard.jsx | 8 ++++++++ .../CippFormPages/CippAddEditUser.jsx | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/components/CippCards/CippUserInfoCard.jsx b/src/components/CippCards/CippUserInfoCard.jsx index f0de3777f71a..97e6917e9126 100644 --- a/src/components/CippCards/CippUserInfoCard.jsx +++ b/src/components/CippCards/CippUserInfoCard.jsx @@ -130,6 +130,14 @@ export const CippUserInfoCard = (props) => { label="Postal code" value={isFetching ? : user?.postalCode || "N/A"} /> + : user?.city || "N/A"} + /> + : user?.country || "N/A"} + /> { formControl={formControl} /> + + + + + + Date: Tue, 13 May 2025 00:03:14 +0200 Subject: [PATCH 014/143] include groups --- .../administration/alert-configuration/alert.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index a07ad0d7e572..41f09ddd481b 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -120,10 +120,11 @@ const AlertWizard = () => { if (usedCommand?.requiresInput && alert.RawAlert.Parameters) { try { // Check if Parameters is a string that needs parsing - const params = typeof alert.RawAlert.Parameters === 'string' - ? JSON.parse(alert.RawAlert.Parameters) - : alert.RawAlert.Parameters; - + const params = + typeof alert.RawAlert.Parameters === "string" + ? JSON.parse(alert.RawAlert.Parameters) + : alert.RawAlert.Parameters; + // Set the input value if it exists if (params.InputValue) { resetObject[usedCommand.inputName] = params.InputValue; @@ -345,6 +346,7 @@ const AlertWizard = () => { formControl={formControl} allTenants={true} label="Included Tenants for alert" + includeGroups={true} /> Date: Tue, 13 May 2025 12:19:03 +0800 Subject: [PATCH 015/143] Fixes Moved validation to existing central validator Moved useWatch outside of useEffect scope Replaced formcontrol.Watch with useWatch --- .../tenant-allow-block-lists/add.jsx | 263 ++++++++++-------- src/utils/get-cipp-validator.js | 24 ++ 2 files changed, 170 insertions(+), 117 deletions(-) diff --git a/src/pages/email/administration/tenant-allow-block-lists/add.jsx b/src/pages/email/administration/tenant-allow-block-lists/add.jsx index d82b261ad03f..99edb083a70d 100644 --- a/src/pages/email/administration/tenant-allow-block-lists/add.jsx +++ b/src/pages/email/administration/tenant-allow-block-lists/add.jsx @@ -1,11 +1,11 @@ -import React from "react"; import { useEffect } from "react"; import { Grid } from "@mui/material"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "../../../../hooks/use-settings"; +import { getCippValidator } from "/src/utils/get-cipp-validator"; const AddTenantAllowBlockList = () => { const tenantDomain = useSettings().currentTenant; @@ -22,51 +22,141 @@ const AddTenantAllowBlockList = () => { }, }); + const noExpiration = useWatch({ control: formControl.control, name: "NoExpiration" }); + const removeAfter = useWatch({ control: formControl.control, name: "RemoveAfter" }); + const listMethod = useWatch({ control: formControl.control, name: "listMethod" }); + const listType = useWatch({ control: formControl.control, name: "listType" }); + + const isListMethodBlock = listMethod?.value === "Block"; + const isListTypeFileHash = listType?.value === "FileHash"; + const isListTypeSenderUrlOrFileHash = ["Sender", "Url", "FileHash"].includes(listType?.value); + const isNoExpirationCompatible = isListMethodBlock || + (listMethod?.value === "Allow" && (listType?.value === "Url" || listType?.value === "IP")); + useEffect(() => { - const subscription = formControl.watch((value, { name }) => { - // If NoExpiration is turned on, disable RemoveAfter - if (name === "NoExpiration" && value.NoExpiration) { - formControl.setValue("RemoveAfter", false); - } - - // If RemoveAfter is turned on, disable NoExpiration - if (name === "RemoveAfter" && value.RemoveAfter) { + if (noExpiration) { + formControl.setValue("RemoveAfter", false); + } + + if (removeAfter) { + formControl.setValue("NoExpiration", false); + } + + if (isListMethodBlock) { + formControl.setValue("RemoveAfter", false); + } + + if (listType && !isListTypeSenderUrlOrFileHash) { + formControl.setValue("RemoveAfter", false); + } + + if (isListTypeFileHash) { + formControl.setValue("listMethod", { label: "Block", value: "Block" }); + } + + if (listMethod || listType) { + if (!isNoExpirationCompatible && noExpiration) { formControl.setValue("NoExpiration", false); } - - // If ListMethod is Block, disable RemoveAfter - if (name === "listMethod" && value.listMethod?.value === "Block") { - formControl.setValue("RemoveAfter", false); + } + }, [ + noExpiration, + removeAfter, + isListMethodBlock, + listType, + isListTypeSenderUrlOrFileHash, + isListTypeFileHash, + isNoExpirationCompatible, + formControl + ]); + + const validateEntries = (value) => { + if (!value) return true; + + const entries = value.split(/[,;]/).map(e => e.trim()); + const currentListType = listType?.value; + + if (currentListType === "FileHash") { + for (const entry of entries) { + if (entry.length !== 64) + return "File hash entries must be exactly 64 characters"; + + const hashResult = getCippValidator(entry, "sha256"); + if (hashResult !== true) + return hashResult; } - - // If listType is not Sender, Url, or FileHash, disable RemoveAfter - if (name === "listType" && !["Sender", "Url", "FileHash"].includes(value.listType?.value)) { - formControl.setValue("RemoveAfter", false); + } else if (currentListType === "IP") { + for (const entry of entries) { + const ipv6Result = getCippValidator(entry, "ipv6"); + const ipv6CidrResult = getCippValidator(entry, "ipv6cidr"); + + if (ipv6Result !== true && ipv6CidrResult !== true) + return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation"; } - - // If listType is FileHash, set ListMethod to Block - if (name === "listType" && value.listType?.value === "FileHash") { - formControl.setValue("listMethod", { label: "Block", value: "Block" }); + } else if (currentListType === "Url") { + for (const entry of entries) { + if (entry.length > 250) + return "URL entries must be 250 characters or less"; + + // For entries with wildcards, use the improved wildcard validators + if (entry.includes('*') || entry.includes('~')) { + // Try both wildcard validators + const wildcardUrlResult = getCippValidator(entry, "wildcardUrl"); + const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); + + if (wildcardUrlResult !== true && wildcardDomainResult !== true) { + // If basic pattern check fails too, give a more specific message + if (!/^[a-zA-Z0-9\.\-\*\~\/]+$/.test(entry)) { + return "Invalid wildcard pattern. Use only letters, numbers, dots, hyphens, slashes, and wildcards (* or ~)"; + } + + // If it has basic valid characters but doesn't match our patterns + return "Invalid wildcard format. Common formats are *.domain.com or domain.*"; + } + continue; + } + + // For non-wildcard entries, use standard validators + const ipv4Result = getCippValidator(entry, "ip"); + const ipv4CidrResult = getCippValidator(entry, "ipv4cidr"); + const ipv6Result = getCippValidator(entry, "ipv6"); + const ipv6CidrResult = getCippValidator(entry, "ipv6cidr"); + const hostnameResult = getCippValidator(entry, "hostname"); + const urlResult = getCippValidator(entry, "url"); + + // If none of the validators pass + if (ipv4Result !== true && + ipv4CidrResult !== true && + ipv6Result !== true && + ipv6CidrResult !== true && + hostnameResult !== true && + urlResult !== true) { + return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses"; + } } - - // Handle NoExpiration compatibility based on rules - const listMethod = value.listMethod?.value; - const listType = value.listType?.value; - - // Check if NoExpiration should be enabled - const isNoExpirationCompatible = - listMethod === "Block" || - (listMethod === "Allow" && (listType === "Url" || listType === "IP")); - - // If current selection is incompatible with NoExpiration, reset it to false - if ((name === "listMethod" || name === "listType") && - !isNoExpirationCompatible && value.NoExpiration) { - formControl.setValue("NoExpiration", false); + } else if (currentListType === "Sender") { + for (const entry of entries) { + // Check for wildcards first + if (entry.includes('*') || entry.includes('~')) { + const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); + + if (wildcardDomainResult !== true) { + return "Invalid sender wildcard pattern. Common format is *.domain.com"; + } + continue; + } + + // For non-wildcard entries, use senderEntry validator + const senderResult = getCippValidator(entry, "senderEntry"); + + if (senderResult !== true) { + return senderResult; + } } - }); + } - return () => subscription.unsubscribe(); - }, [formControl]); + return true; + }; return ( { formControl={formControl} validators={{ required: "Entries field is required", - validate: (value) => { - if (!value) return true; - - const entries = value.split(/[,;]/).map(e => e.trim()); - const listType = formControl.watch("listType")?.value; - - if (listType === "FileHash") { - // SHA256 hash validation - 64 hex characters - for (const entry of entries) { - if (entry.length !== 64) - return "File hash entries must be exactly 64 characters"; - if (!/^[A-Fa-f0-9]{64}$/.test(entry)) - return "File hash must contain only hexadecimal characters"; - } - } else if (listType === "IP") { - // IPv6 address validation (for IP list type - IPv6 only) - const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/; - const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/; - - for (const entry of entries) { - if (!ipv6Regex.test(entry) && !ipv6CidrRegex.test(entry)) - return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation"; - } - } else if (listType === "Url") { - // For URL list type - check length and validate any IP addresses - // IPv4 regex - const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/; - // IPv4 CIDR regex - const ipv4CidrRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}\/([0-9]|[12][0-9]|3[0-2])$/; - // IPv6 regex (same as above) - const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/; - const ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/; - - // Hostname regex (allowing wildcards * and ~) - const hostnameRegex = /^((\*|\~)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\*|\~)?$/; - - for (const entry of entries) { - if (entry.length > 250) - return "URL entries must be 250 characters or less"; - - // Skip validation for entries with wildcards (more complex patterns) - if (entry.includes('*') || entry.includes('~')) continue; - - // If it's not a hostname and not an IP, it might be invalid - if (!ipv4Regex.test(entry) && !ipv4CidrRegex.test(entry) && - !ipv6Regex.test(entry) && !ipv6CidrRegex.test(entry) && - !hostnameRegex.test(entry)) { - // Allow some flexibility for wildcard patterns - // But at least provide a warning for obviously invalid formats - if (!/[a-zA-Z0-9\.\-\*\~]/.test(entry)) { - return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses"; - } - } - } - } - - return true; - } + validate: validateEntries }} helperText={ - formControl.watch("listType")?.value === "FileHash" - ? "Enter SHA256 hash values separated by commas or semicolons (e.g., 768a813668695ef2483b2bde7cf5d1b2db0423a0d3e63e498f3ab6f2eb13ea3)" - : formControl.watch("listType")?.value === "Url" + listType?.value === "FileHash" + ? "Enter SHA256 hash values separated by commas or semicolons (e.g., 768a813668695ef2483b2bde7cf5d1b2db0423a0d3e63e498f3ab6f2eb13ea3e)" + : listType?.value === "Url" ? "Enter URLs, IPv4, or IPv6 addresses with optional wildcards separated by commas or semicolons" - : formControl.watch("listType")?.value === "Sender" + : listType?.value === "Sender" ? "Enter domains or email addresses separated by commas or semicolons (e.g., contoso.com,user@example.com)" - : formControl.watch("listType")?.value === "IP" + : listType?.value === "IP" ? "Enter IPv6 addresses only in colon-hexadecimal format or CIDR notation" : "" } @@ -210,13 +243,9 @@ const AddTenantAllowBlockList = () => { { label: "Allow", value: "Allow" }, ]} validators={{ required: "Please select Block or Allow." }} - disabled={ - formControl.watch("listType")?.value === "FileHash" - ? true - : false - } + disabled={isListTypeFileHash} helperText={ - formControl.watch("listType")?.value === "FileHash" + isListTypeFileHash ? "FileHash entries can only be Blocked" : "Choose whether to block or allow the entries" } @@ -231,16 +260,16 @@ const AddTenantAllowBlockList = () => { name="NoExpiration" formControl={formControl} helperText={ - formControl.watch("listMethod")?.value === "Block" + isListMethodBlock ? "Block entries will never expire" : "Available only for Block entries or specific Allow entries (URL/IP)" } disabled={ - formControl.watch("RemoveAfter") || - !(formControl.watch("listMethod")?.value === "Block" || - (formControl.watch("listMethod")?.value === "Allow" && - (formControl.watch("listType")?.value === "Url" || - formControl.watch("listType")?.value === "IP"))) + removeAfter || + !(isListMethodBlock || + (listMethod?.value === "Allow" && + (listType?.value === "Url" || + listType?.value === "IP"))) } /> @@ -254,9 +283,9 @@ const AddTenantAllowBlockList = () => { formControl={formControl} helperText="If checked, allow entries will be removed after 45 days of last use" disabled={ - formControl.watch("NoExpiration") || - formControl.watch("listMethod")?.value !== "Allow" || - !["Sender", "FileHash", "Url"].includes(formControl.watch("listType")?.value) + noExpiration || + listMethod?.value !== "Allow" || + !["Sender", "FileHash", "Url"].includes(listType?.value) } /> diff --git a/src/utils/get-cipp-validator.js b/src/utils/get-cipp-validator.js index 334bbe7dce49..3b42fb6c5101 100644 --- a/src/utils/get-cipp-validator.js +++ b/src/utils/get-cipp-validator.js @@ -19,11 +19,35 @@ export const getCippValidator = (value, type) => { return typeof value === "string" || "This is not a valid string"; case "ip": return /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value) || "This is not a valid IP address"; + case "ipv4cidr": + return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}\/([0-9]|[12][0-9]|3[0-2])$/.test(value) || "This is not a valid IPv4 CIDR"; + case "ipv6": + return /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/.test(value) || "This is not a valid IPv6 address"; + case "ipv6cidr": + return /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/.test(value) || "This is not a valid IPv6 CIDR"; + case "hostname": + return /^((\*|\~)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\*|\~)?$/.test(value) || "This is not a valid hostname"; + case "sha256": + return /^[A-Fa-f0-9]{64}$/.test(value) || "This is not a valid SHA256 hash"; case "guid": return ( /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) || "This is not a valid GUID" ); + case "domain": + return /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/.test(value) || "This is not a valid domain"; + case "wildcardDomain": + return /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.\*)?$/.test(value) || /^(\*)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\*)?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/.test(value) || "This is not a valid domain pattern"; + case "wildcardUrl": + return /^(https?:\/\/)?(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.\*)?([\/\?\*][a-zA-Z0-9\-\.\~\*\/\?=&%]*)?$/.test(value) || "This is not a valid URL pattern"; + case "emailAddress": + return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) || "This is not a valid email address"; + case "senderEntry": + return ( + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/.test(value) || + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) || + "This is not a valid domain or email address" + ); default: return true; } From d23002610f70f8ae81725d5fbd31519d79b88033 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 13 May 2025 12:35:17 +0800 Subject: [PATCH 016/143] Indentation fix for timezoneList.json --- src/data/timezoneList.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/timezoneList.json b/src/data/timezoneList.json index d259bc80e65d..57238fe7eedc 100644 --- a/src/data/timezoneList.json +++ b/src/data/timezoneList.json @@ -189,7 +189,7 @@ "timezone": "(UTC+04:00) Astrakhan, Ulyanovsk" }, { - "timezone": "(UTC+04:00) Baku" + "timezone": "(UTC+04:00) Baku" }, { "timezone": "(UTC+04:00) Izhevsk, Samara" From 872e2a9f70b43eaa3c41d0668c60d8d30bd2856a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 13 May 2025 16:54:38 +0200 Subject: [PATCH 017/143] feat: Add new Set Exchange Outbound Spam Limits standard --- src/data/standards.json | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 6abcb77d9791..3a75fdc58266 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -1371,6 +1371,54 @@ "powershellEquivalent": "Set-MailboxFolderPermission", "recommendedBy": [] }, + { + "name": "standards.EXOOutboundSpamLimits", + "cat": "Exchange Standards", + "tag": ["CIS"], + "helpText": "Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. ", + "docsDescription": "Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one.", + "addedComponent": [ + { + "type": "number", + "name": "standards.EXOOutboundSpamLimits.RecipientLimitExternalPerHour", + "label": "External Recipient Limit Per Hour", + "defaultValue": 400 + }, + { + "type": "number", + "name": "standards.EXOOutboundSpamLimits.RecipientLimitInternalPerHour", + "label": "Internal Recipient Limit Per Hour", + "defaultValue": 800 + }, + { + "type": "number", + "name": "standards.EXOOutboundSpamLimits.RecipientLimitPerDay", + "label": "Daily Recipient Limit", + "defaultValue": 800 + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "name": "standards.EXOOutboundSpamLimits.ActionWhenThresholdReached", + "label": "Action When Threshold Reached", + "options": [ + { "label": "Alert", "value": "Alert" }, + { "label": "Block User", "value": "BlockUser" }, + { + "label": "Block user from sending mail for the rest of the day", + "value": "BlockUserForToday" + } + ] + } + ], + "label": "Set Exchange Outbound Spam Limits", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-05-13", + "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy", + "recommendedBy": ["CIPP", "CIS"] + }, { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", From 4c50e24706f50a785c762546227e56106246cc38 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 May 2025 18:28:36 +0200 Subject: [PATCH 018/143] add persistor --- src/api/ApiCall.jsx | 12 +++++++++--- src/pages/_app.js | 32 +++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index 936654ff16fd..07c80d9c211f 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -1,4 +1,10 @@ -import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + keepPreviousData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import axios, { isAxiosError } from "axios"; import { useDispatch } from "react-redux"; import { showToast } from "../store/toasts"; @@ -15,7 +21,7 @@ export function ApiGetCall(props) { bulkRequest = false, toast = false, onResult, - staleTime = 600000, // 10 minutes + staleTime = 300000, refetchOnWindowFocus = false, refetchOnMount = true, refetchOnReconnect = true, @@ -212,7 +218,7 @@ export function ApiGetCallWithPagination({ } return lastPage?.Metadata?.nextLink ? { nextLink: lastPage.Metadata.nextLink } : undefined; }, - staleTime: 600000, // 10 minutes + staleTime: 30000, refetchOnWindowFocus: false, retry: retryFn, }); diff --git a/src/pages/_app.js b/src/pages/_app.js index bf72bf37c6a3..44b490dfd2ff 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -29,17 +29,13 @@ import { } from "@mui/icons-material"; import { SvgIcon } from "@mui/material"; import discordIcon from "../../public/discord-mark-blue.svg"; -import React from "react"; +import React, { useEffect } from "react"; import { usePathname } from "next/navigation"; import { useRouter } from "next/router"; +import { persistQueryClient } from "@tanstack/react-query-persist-client"; +import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; TimeAgo.addDefaultLocale(en); -const ReactQueryDevtoolsProduction = React.lazy(() => - import("@tanstack/react-query-devtools/build/modern/production.js").then((d) => ({ - default: d.ReactQueryDevtools, - })) -); - const queryClient = new QueryClient(); const clientSideEmotionCache = createEmotionCache(); const App = (props) => { @@ -49,6 +45,22 @@ const App = (props) => { const pathname = usePathname(); const route = useRouter(); + // 👇 Persist TanStack Query cache to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + const localStoragePersister = createSyncStoragePersister({ + storage: window.localStorage, + }); + + persistQueryClient({ + queryClient, + persister: localStoragePersister, + maxAge: 1000 * 60 * 60 * 24, // 24 hours + buster: "v1", + }); + } + }, []); + const speedDialActions = [ { id: "license", @@ -98,6 +110,12 @@ const App = (props) => { }, ]; + const ReactQueryDevtoolsProduction = React.lazy(() => + import("@tanstack/react-query-devtools/build/modern/production.js").then((d) => ({ + default: d.ReactQueryDevtools, + })) + ); + return ( From b08b079725abf2fc1d5d4eaedb785b247c2d3a33 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 May 2025 18:29:14 +0200 Subject: [PATCH 019/143] added package --- package.json | 2 ++ yarn.lock | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/package.json b/package.json index 879b3c54028c..934681339eba 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,10 @@ "@musement/iso-duration": "^1.0.0", "@react-pdf/renderer": "4.3.0", "@reduxjs/toolkit": "2.6.1", + "@tanstack/query-sync-storage-persister": "^5.76.0", "@tanstack/react-query": "^5.51.11", "@tanstack/react-query-devtools": "^5.51.11", + "@tanstack/react-query-persist-client": "^5.76.0", "@tanstack/react-table": "^8.19.2", "@tiptap/core": "^2.9.1", "@tiptap/extension-heading": "^2.9.1", diff --git a/yarn.lock b/yarn.lock index dcb80c7f0332..d809e0a29713 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1872,11 +1872,31 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.69.0.tgz#c434505987ade936dc53e6e27aa1406b0295516f" integrity sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ== +"@tanstack/query-core@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.76.0.tgz#3b4d5d34ce307ba0cf7d1a3e90d7adcdc6c46be0" + integrity sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg== + "@tanstack/query-devtools@5.67.2": version "5.67.2" resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.67.2.tgz#890ae9913bd21d3969c7fd85c68b1bd1500cfc57" integrity sha512-O4QXFFd7xqp6EX7sdvc9tsVO8nm4lpWBqwpgjpVLW5g7IeOY6VnS/xvs/YzbRhBVkKTMaJMOUGU7NhSX+YGoNg== +"@tanstack/query-persist-client-core@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.76.0.tgz#a3bcdd687384dc6b5b61b402bef153ad54515321" + integrity sha512-xcTZjILf4q49Nsl6wcnhBYZ4O0gpnuNwV6vPIEWIrwTuSNWz2zd/g9bc8SxnXy7xCV8SM1H0IJn8KjLQIUb2ag== + dependencies: + "@tanstack/query-core" "5.76.0" + +"@tanstack/query-sync-storage-persister@^5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.76.0.tgz#29643062f1a424873afb22032ce70ee72436bb9b" + integrity sha512-N8d8voY61XkM+jfXTySduLrevD6wRM3pwQ1kG0syLiWWx/sX2+CpaTMSPr0GggjQuhmjhUPo83LaV+e449tizA== + dependencies: + "@tanstack/query-core" "5.76.0" + "@tanstack/query-persist-client-core" "5.76.0" + "@tanstack/react-query-devtools@^5.51.11": version "5.69.0" resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.69.0.tgz#2cb8083028aab591b9a82caf68cd7a383a0c8b1a" @@ -1884,6 +1904,13 @@ dependencies: "@tanstack/query-devtools" "5.67.2" +"@tanstack/react-query-persist-client@^5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.76.0.tgz#97718fec844708cb98a5902d4b1eeb72adea555b" + integrity sha512-QPKgkHX1yC1Ec21FTQHBTbQcHYI+6157DgsmxABp94H7/ZUJ3szZ7wdpdBPQyZ9VxBXlKRN+aNZkOPC90+r/uA== + dependencies: + "@tanstack/query-persist-client-core" "5.76.0" + "@tanstack/react-query@^5.51.11": version "5.69.0" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.69.0.tgz#8d58e800854cc11d0aa2c39569f53ae32ba442a9" From 79eafc69f82cefda1c89812f82592a7f413d9b6b Mon Sep 17 00:00:00 2001 From: Chris Brannon Date: Tue, 13 May 2025 12:40:22 -0400 Subject: [PATCH 020/143] Next Backup --- src/pages/cipp/settings/backup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/cipp/settings/backup.js b/src/pages/cipp/settings/backup.js index 6935872881c2..b4f633a74e90 100644 --- a/src/pages/cipp/settings/backup.js +++ b/src/pages/cipp/settings/backup.js @@ -55,7 +55,7 @@ const Page = () => { }); const NextBackupRun = (props) => { - const date = new Date(props.date * 1000); + const date = new Date(props.date); if (isNaN(date)) { return "Not Scheduled"; } else { From 892d2482bfc8888cba9a7d10c9f103e7cd1d6955 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 May 2025 18:42:07 +0200 Subject: [PATCH 021/143] fix early stales --- src/api/ApiCall.jsx | 2 +- src/pages/_app.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index 07c80d9c211f..35554764168e 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -218,7 +218,7 @@ export function ApiGetCallWithPagination({ } return lastPage?.Metadata?.nextLink ? { nextLink: lastPage.Metadata.nextLink } : undefined; }, - staleTime: 30000, + staleTime: 300000, refetchOnWindowFocus: false, retry: retryFn, }); diff --git a/src/pages/_app.js b/src/pages/_app.js index 44b490dfd2ff..2894e590fb18 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -56,6 +56,7 @@ const App = (props) => { queryClient, persister: localStoragePersister, maxAge: 1000 * 60 * 60 * 24, // 24 hours + staleTime: 1000 * 60 * 5, // optional: 5 minutes buster: "v1", }); } From 3ebdcb00187d48e7f16054d1fc0dde764af594d3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 May 2025 19:04:42 +0200 Subject: [PATCH 022/143] weirdness with dev and tools --- src/pages/_app.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/pages/_app.js b/src/pages/_app.js index 2894e590fb18..6d585563825b 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -35,6 +35,7 @@ import { useRouter } from "next/router"; import { persistQueryClient } from "@tanstack/react-query-persist-client"; import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; TimeAgo.addDefaultLocale(en); +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; const queryClient = new QueryClient(); const clientSideEmotionCache = createEmotionCache(); @@ -111,12 +112,6 @@ const App = (props) => { }, ]; - const ReactQueryDevtoolsProduction = React.lazy(() => - import("@tanstack/react-query-devtools/build/modern/production.js").then((d) => ({ - default: d.ReactQueryDevtools, - })) - ); - return ( @@ -163,7 +158,7 @@ const App = (props) => { {settings.isInitialized && settings?.showDevtools === true ? ( - + ) : null} From c1c10e7739d491220caf99319f00667c04462313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 13 May 2025 20:30:17 +0200 Subject: [PATCH 023/143] Add "Edit permissions" action to room management page --- src/pages/email/resources/management/list-rooms/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/email/resources/management/list-rooms/index.js b/src/pages/email/resources/management/list-rooms/index.js index bbf9e9f2d1c2..68c79ba360c2 100644 --- a/src/pages/email/resources/management/list-rooms/index.js +++ b/src/pages/email/resources/management/list-rooms/index.js @@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; -import { AddHomeWork, Edit, Block, LockOpen } from "@mui/icons-material"; +import { AddHomeWork, Edit, Block, LockOpen, Key } from "@mui/icons-material"; import { TrashIcon } from "@heroicons/react/24/outline"; const Page = () => { @@ -16,6 +16,12 @@ const Page = () => { color: "info", condition: (row) => !row.isDirSynced, }, + { + label: "Edit permissions", + link: "/identity/administration/users/user/exchange?userId=[id]", + color: "info", + icon: , + }, { label: "Block Sign In", type: "POST", From e2f60422dd6b15d16a24e9ba1d9d62511a345b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 13 May 2025 22:11:49 +0200 Subject: [PATCH 024/143] feat: Add validation messages for user selection, policy selection, and end date input --- src/pages/tenant/conditional/deploy-vacation/add.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/tenant/conditional/deploy-vacation/add.jsx b/src/pages/tenant/conditional/deploy-vacation/add.jsx index 5e64bb392ea3..dda5279515f2 100644 --- a/src/pages/tenant/conditional/deploy-vacation/add.jsx +++ b/src/pages/tenant/conditional/deploy-vacation/add.jsx @@ -56,6 +56,7 @@ const Page = () => { formControl={formControl} name="UserId" multiple={false} + validators={{ required: "Picking a user is required" }} /> @@ -73,6 +74,7 @@ const Page = () => { }} multiple={false} formControl={formControl} + validators={{ required: "Picking a policy is required" }} /> @@ -95,6 +97,7 @@ const Page = () => { name="endDate" dateTimeType="dateTime" formControl={formControl} + validators={{ required: "Picking an end date is required" }} /> From 574ace793c5218624c7635c31962a47f745e838f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 14 May 2025 22:41:56 +0800 Subject: [PATCH 025/143] Update backup.js Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- src/pages/cipp/settings/backup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/cipp/settings/backup.js b/src/pages/cipp/settings/backup.js index b4f633a74e90..9c81366d5712 100644 --- a/src/pages/cipp/settings/backup.js +++ b/src/pages/cipp/settings/backup.js @@ -135,6 +135,7 @@ const Page = () => { confirmText: "Are you sure you want to restore this backup?", relatedQueryKeys: ["BackupList"], multiPost: false, + hideBulk: true, }, { label: "Download Backup", From f851ca841a41ee6b38bda36f5cdfb9f25c93de31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 14 May 2025 16:56:40 +0200 Subject: [PATCH 026/143] update icons import and add permanently delete action --- .../administration/deleted-items/index.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/pages/identity/administration/deleted-items/index.js b/src/pages/identity/administration/deleted-items/index.js index b82213e9e8c9..66d27c8bfd83 100644 --- a/src/pages/identity/administration/deleted-items/index.js +++ b/src/pages/identity/administration/deleted-items/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import RestoreFromTrashIcon from "@mui/icons-material/RestoreFromTrash"; +import { RestoreFromTrash, Warning } from "@mui/icons-material"; const Page = () => { const pageTitle = "Deleted Items"; @@ -9,10 +9,20 @@ const Page = () => { { label: "Restore Object", type: "POST", - icon: , + icon: , url: "/api/ExecRestoreDeleted", - data: { ID: "id" }, - confirmText: "Are you sure you want to restore this user?", + data: { ID: "id", userPrincipalName: "userPrincipalName", displayName: "displayName" }, + confirmText: "Are you sure you want to restore this object?", + multiPost: false, + }, + { + label: "Permanently Delete Object", + type: "POST", + icon: , + url: "/api/RemoveDeletedObject", + data: { ID: "id", userPrincipalName: "userPrincipalName", displayName: "displayName" }, + confirmText: + "Are you sure you want to permanently delete this object? This action cannot be undone.", multiPost: false, }, ]; From 70dfb409c48209561802768a7d0c1dce15172395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 14 May 2025 17:49:06 +0200 Subject: [PATCH 027/143] Enhance user deletion logging --- src/components/CippComponents/CippUserActions.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 4b0f5fe1b6c9..1f7dea46433e 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -156,7 +156,7 @@ export const CippUserActions = () => { url: "/api/ExecSetOoO", data: { userId: "userPrincipalName", - AutoReplyState: { value: "Disabled" } + AutoReplyState: { value: "Disabled" }, }, confirmText: "Are you sure you want to disable the out of office?", multiPost: false, @@ -311,7 +311,7 @@ export const CippUserActions = () => { type: "POST", icon: , url: "/api/RemoveUser", - data: { ID: "id" }, + data: { ID: "id", userPrincipalName: "userPrincipalName" }, confirmText: "Are you sure you want to delete this user?", multiPost: false, }, From 56cfb643343e107b95180b2ecd99db84f9f9b462 Mon Sep 17 00:00:00 2001 From: lsmith090 <47199231+lsmith090@users.noreply.github.com> Date: Wed, 14 May 2025 13:34:01 -0400 Subject: [PATCH 028/143] fix validators prop --- src/components/CippComponents/CippCustomVariables.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippCustomVariables.jsx b/src/components/CippComponents/CippCustomVariables.jsx index ce27670a9aed..0f75743a8029 100644 --- a/src/components/CippComponents/CippCustomVariables.jsx +++ b/src/components/CippComponents/CippCustomVariables.jsx @@ -50,7 +50,7 @@ const CippCustomVariables = ({ id }) => { label: "Variable Name", placeholder: "Enter the key for the custom variable.", required: true, - validators: validateVariableName, + validators: { validate: validateVariableName }, }, { type: "textField", @@ -134,7 +134,7 @@ const CippCustomVariables = ({ id }) => { label: "Variable Name", placeholder: "Enter the name for the custom variable without %.", required: true, - validators: validateVariableName, + validators: { validate: validateVariableName }, }, { type: "textField", From 4b684916a1c1a1408b20d039c9afd4c8e4053700 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 15 May 2025 17:14:08 +0800 Subject: [PATCH 029/143] Update CippWizardOffboarding.jsx --- src/components/CippWizard/CippWizardOffboarding.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx index 4faeb57318c2..b543618c883e 100644 --- a/src/components/CippWizard/CippWizardOffboarding.jsx +++ b/src/components/CippWizard/CippWizardOffboarding.jsx @@ -277,7 +277,7 @@ export const CippWizardOffboarding = (props) => { {showAlert && ( - You have selected more than 3 users. This offboarding must be scheduled. + You have selected more than 2 users. This offboarding must be scheduled. )} From 89f371738d229fcc644bda5f4965ad60bafb7849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 15 May 2025 18:29:48 +0200 Subject: [PATCH 030/143] feat:Add action to set max send/receive size for mailboxes --- .../CippComponents/CippExchangeActions.jsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx index b947a19603fa..cbe8f656e633 100644 --- a/src/components/CippComponents/CippExchangeActions.jsx +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -15,6 +15,7 @@ import { NotificationImportant, DataUsage, MailLock, + SettingsEthernet, } from "@mui/icons-material"; export const CippExchangeActions = () => { @@ -219,6 +220,28 @@ export const CippExchangeActions = () => { }, ], }, + { + label: "Set Max Send/Receive Size", + type: "POST", + url: "/api/ExecSetMailboxEmailSize", + data: { UPN: "UPN", id: "ExternalDirectoryObjectId" }, + confirmText: "Enter a size in from 1 to 150. Leave blank to not change.", + icon: , + fields: [ + { + label: "Send Size(MB)", + name: "maxSendSize", + type: "number", + placeholder: "e.g. 35", + }, + { + label: "Receive Size(MB)", + name: "maxReceiveSize", + type: "number", + placeholder: "e.g. 36", + }, + ], + }, { label: "Set Send Quota", type: "POST", From 8876672e9bc82636ec4c56e4f2ed4acfd40ab77b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 15 May 2025 23:35:34 +0200 Subject: [PATCH 031/143] Feat: Add "Change Primary User" action to Intune device management --- src/pages/endpoint/MEM/devices/index.js | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 17197aff1e9b..e3a66509ea79 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -15,6 +15,7 @@ import { Archive, AutoMode, Recycling, + ManageAccounts, } from "@mui/icons-material"; const Page = () => { @@ -31,6 +32,43 @@ const Page = () => { multiPost: false, external: true, }, + { + label: "Change Primary User", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "!users", + }, + fields: [ + { + type: "autoComplete", + name: "user", + label: "Select User", + multiple: false, + creatable: false, + api: { + url: "/api/ListGraphRequest", + data: { + Endpoint: "users", + $select: "id,displayName,userPrincipalName", + $top: 999, + $count: true, + }, + queryKey: "ListUsersAutoComplete", + dataKey: "Results", + labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, + valueField: "id", + addedField: { + userPrincipalName: "userPrincipalName", + }, + showRefresh: true, + }, + }, + ], + confirmText: "Select the User to set as the primary user for this device", + }, { label: "Sync Device", type: "POST", From 77bffa9a58737d14f89e96317fca765a3a8794fd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 16 May 2025 02:08:27 +0200 Subject: [PATCH 032/143] MSAL magic --- .../CippComponents/CIPPM365OAuthButton.js | 469 ++++++++++++++++++ .../CippWizard/CIPPDeploymentStep.js | 69 +-- .../CippWizard/CIPPDeploymentUpdateTokens.js | 57 +++ src/pages/onboardingv2.js | 107 ++++ 4 files changed, 636 insertions(+), 66 deletions(-) create mode 100644 src/components/CippComponents/CIPPM365OAuthButton.js create mode 100644 src/components/CippWizard/CIPPDeploymentUpdateTokens.js create mode 100644 src/pages/onboardingv2.js diff --git a/src/components/CippComponents/CIPPM365OAuthButton.js b/src/components/CippComponents/CIPPM365OAuthButton.js new file mode 100644 index 000000000000..9d162aba8971 --- /dev/null +++ b/src/components/CippComponents/CIPPM365OAuthButton.js @@ -0,0 +1,469 @@ +import { useState } from "react"; +import { + Alert, + Button, + Stack, + Typography, + CircularProgress, + Box, +} from "@mui/material"; +import { ApiGetCall } from "../../api/ApiCall"; + +/** + * CIPPM365OAuthButton - A reusable button component for Microsoft 365 OAuth authentication + * + * @param {Object} props - Component props + * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data + * @param {Function} props.onAuthError - Callback function called when authentication fails with error data + * @param {string} props.buttonText - Text to display on the button (default: "Login with Microsoft") + * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true) + * @param {string} props.scope - OAuth scope to request (default: "https://graph.microsoft.com/.default offline_access profile openid") + * @returns {JSX.Element} The CIPPM365OAuthButton component + */ +export const CIPPM365OAuthButton = ({ + onAuthSuccess, + onAuthError, + buttonText = "Login with Microsoft", + showResults = true, + scope = "https://graph.microsoft.com/.default offline_access profile openid", +}) => { + const [authInProgress, setAuthInProgress] = useState(false); + const [authError, setAuthError] = useState(null); + const [tokens, setTokens] = useState({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + // Get application ID information + const appId = ApiGetCall({ + url: `/api/ExecListAppId`, + queryKey: `ExecListAppId`, + waiting: true, + }); + + // Handle closing the error + const handleCloseError = () => { + setAuthError(null); + }; + + // MSAL-like authentication function + const handleMsalAuthentication = () => { + // Clear previous authentication state when starting a new authentication + setAuthInProgress(true); + setAuthError(null); + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + // Generate MSAL-like authentication parameters + const msalConfig = { + auth: { + clientId: appId?.data?.applicationId, + authority: `https://login.microsoftonline.com/common`, + redirectUri: window.location.origin, + }, + }; + + // Define the request object similar to MSAL + const loginRequest = { + scopes: [scope], + }; + + console.log("MSAL Config:", msalConfig); + console.log("Login Request:", loginRequest); + + // Generate PKCE code verifier and challenge + const generateCodeVerifier = () => { + const array = new Uint8Array(32); + window.crypto.getRandomValues(array); + return Array.from(array, (byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)).join(""); + }; + + const base64URLEncode = (str) => { + return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + }; + + // Generate code verifier for PKCE + const codeVerifier = generateCodeVerifier(); + // In a real implementation, we would hash the code verifier to create the code challenge + // For simplicity, we'll use the same value + const codeChallenge = codeVerifier; + + // Note: We're not storing the code verifier in session storage for security reasons + // Instead, we'll use it directly in the token exchange + + // Create a random state value for security + const state = Math.random().toString(36).substring(2, 15); + + // Create the auth URL with PKCE parameters + const authUrl = + `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + + `client_id=${appId?.data?.applicationId}` + + `&response_type=code` + + `&redirect_uri=${encodeURIComponent(window.location.origin)}` + + `&scope=${encodeURIComponent(scope)}` + + `&code_challenge=${codeChallenge}` + + `&code_challenge_method=plain` + + `&state=${state}` + + `&prompt=select_account`; + + console.log("MSAL Auth URL:", authUrl); + + // Open popup for authentication + const width = 500; + const height = 600; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + authUrl, + "msalAuthPopup", + `width=${width},height=${height},left=${left},top=${top}` + ); + + // Function to actually exchange the authorization code for tokens + const handleAuthorizationCode = async (code, receivedState) => { + // Verify the state parameter matches what we sent (security check) + if (receivedState !== state) { + const errorMessage = "State mismatch in auth response - possible CSRF attack"; + console.error(errorMessage); + const error = { + errorCode: "state_mismatch", + errorMessage: errorMessage, + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + setAuthInProgress(false); + return; + } + + console.log("Authorization code received:", code); + + try { + // Actually exchange the code for tokens using the token endpoint + console.log("Exchanging authorization code for tokens..."); + + // Prepare the token request + const tokenRequest = { + grant_type: "authorization_code", + client_id: appId?.data?.applicationId, + code: code, + redirect_uri: window.location.origin, + code_verifier: codeVerifier, + }; + + // Make the token request + const tokenResponse = await fetch( + `https://login.microsoftonline.com/common/oauth2/v2.0/token`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(tokenRequest).toString(), + } + ); + + // Parse the token response + const tokenData = await tokenResponse.json(); + + if (tokenResponse.ok) { + // Extract token information + const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000); + // Refresh tokens typically last for 90 days, but this can vary + // For demonstration, we'll set it to 90 days from now + const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); + + // Extract information from ID token if available + let username = "unknown user"; + let tenantId = appId?.data?.tenantId || "unknown tenant"; + let onmicrosoftDomain = null; + + if (tokenData.id_token) { + try { + const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1])); + + // Extract username + username = + idTokenPayload.preferred_username || + idTokenPayload.email || + idTokenPayload.upn || + idTokenPayload.name || + "unknown user"; + + // Extract tenant ID if available in the token + if (idTokenPayload.tid) { + tenantId = idTokenPayload.tid; + } + + // Try to extract onmicrosoft domain from the username or issuer + if (username && username.includes("@") && username.includes(".onmicrosoft.com")) { + onmicrosoftDomain = username.split("@")[1]; + } else if (idTokenPayload.iss) { + const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//); + if (issuerMatch && issuerMatch[1]) { + // We have the tenant ID, but not the domain name + // We could potentially make an API call to get the domain, but for now we'll leave it null + } + } + } catch (error) { + console.error("Error parsing ID token:", error); + } + } + + // Create token result object + const tokenResult = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + accessTokenExpiresOn: accessTokenExpiresOn, + refreshTokenExpiresOn: refreshTokenExpiresOn, + username: username, + tenantId: tenantId, + onmicrosoftDomain: onmicrosoftDomain, + }; + + // Store tokens in component state + setTokens(tokenResult); + + // Log only the necessary token information to console + console.log("Access Token:", tokenData.access_token); + console.log("Refresh Token:", tokenData.refresh_token); + + // Call the onAuthSuccess callback if provided + if (onAuthSuccess) onAuthSuccess(tokenResult); + } else { + // Handle token error - display in error box instead of throwing + const error = { + errorCode: tokenData.error || "token_error", + errorMessage: + tokenData.error_description || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + } + } catch (error) { + console.error("Error exchanging code for tokens:", error); + const errorObj = { + errorCode: "token_exchange_error", + errorMessage: error.message || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(errorObj); + if (onAuthError) onAuthError(errorObj); + } finally { + // Close the popup window if it's still open + if (popup && !popup.closed) { + popup.close(); + } + + // Update UI state + setAuthInProgress(false); + } + }; + + // Monitor for the redirect with the authorization code + // This is what MSAL does internally + const checkPopupLocation = setInterval(() => { + if (!popup || popup.closed) { + clearInterval(checkPopupLocation); + + // If authentication is still in progress when popup closes, it's an error + if (authInProgress) { + const errorMessage = "Authentication was cancelled. Please try again."; + console.error(errorMessage); + const error = { + errorCode: "user_cancelled", + errorMessage: errorMessage, + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + + // Ensure we're not showing any previous success state + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + } + + setAuthInProgress(false); + return; + } + + try { + // Try to access the popup location to check for the authorization code + const currentUrl = popup.location.href; + + // Check if the URL contains a code parameter (authorization code) + if (currentUrl.includes("code=") && currentUrl.includes("state=")) { + clearInterval(checkPopupLocation); + + console.log("Detected authorization code in URL:", currentUrl); + + // Parse the URL to extract the code and state + const urlParams = new URLSearchParams(popup.location.search); + const code = urlParams.get("code"); + const receivedState = urlParams.get("state"); + + // Process the authorization code + handleAuthorizationCode(code, receivedState); + } + + // Check for error in the URL + if (currentUrl.includes("error=")) { + clearInterval(checkPopupLocation); + + console.error("Detected error in authentication response:", currentUrl); + + // Parse the URL to extract the error details + const urlParams = new URLSearchParams(popup.location.search); + const errorCode = urlParams.get("error"); + const errorDescription = urlParams.get("error_description"); + + // Set the error state + const error = { + errorCode: errorCode, + errorMessage: errorDescription || "Unknown authentication error", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + + // Close the popup + popup.close(); + setAuthInProgress(false); + } + } catch (error) { + // This will throw an error when the popup is on a different domain + // due to cross-origin restrictions, which is normal during auth flow + // Just continue monitoring + } + }, 500); + + // Also monitor for popup closing as a fallback + const checkPopupClosed = setInterval(() => { + if (popup.closed) { + clearInterval(checkPopupClosed); + clearInterval(checkPopupLocation); + + // If authentication is still in progress when popup closes, it's an error + if (authInProgress) { + const errorMessage = "Authentication was cancelled. Please try again."; + console.error(errorMessage); + const error = { + errorCode: "user_cancelled", + errorMessage: errorMessage, + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + + // Ensure we're not showing any previous success state + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + } + + setAuthInProgress(false); + } + }, 1000); + }; + + return ( +
+ + + {!appId.isLoading && + !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( + appId?.data?.applicationId + ) && ( + + The Application ID is not valid. Please check your configuration. + + ) + } + + {showResults && ( + + {tokens.accessToken ? ( + + Authentication Successful + + You've successfully refreshed your token. The account you're using for authentication + is: {tokens.username} + + + Tenant ID: {tokens.tenantId} + {tokens.onmicrosoftDomain && ( + <> | Domain: {tokens.onmicrosoftDomain} + )} + + + Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()} + + + ) : authError ? ( + + Authentication Error: {authError.errorCode} + {authError.errorMessage} + + Time: {authError.timestamp} + + + + + + ) : null} + + )} +
+ ); +}; + +export default CIPPM365OAuthButton; \ No newline at end of file diff --git a/src/components/CippWizard/CIPPDeploymentStep.js b/src/components/CippWizard/CIPPDeploymentStep.js index 070b38811dbf..6ecb6d560a4e 100644 --- a/src/components/CippWizard/CIPPDeploymentStep.js +++ b/src/components/CippWizard/CIPPDeploymentStep.js @@ -16,10 +16,11 @@ import { CippWizardStepButtons } from "./CippWizardStepButtons"; import { ApiGetCall } from "../../api/ApiCall"; import CippButtonCard from "../CippCards/CippButtonCard"; import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; -import { CheckCircle, OpenInNew, Sync } from "@mui/icons-material"; +import { CheckCircle } from "@mui/icons-material"; import CippPermissionCheck from "../CippSettings/CippPermissionCheck"; import { useQueryClient } from "@tanstack/react-query"; import { CippApiResults } from "../CippComponents/CippApiResults"; +import { CIPPDeploymentUpdateTokens } from "./CIPPDeploymentUpdateTokens"; export const CippDeploymentStep = (props) => { const queryClient = useQueryClient(); @@ -40,11 +41,6 @@ export const CippDeploymentStep = (props) => { queryKey: `checkSetupStep${pollingStep}`, waiting: !pollingStep, }); - const appId = ApiGetCall({ - url: `/api/ExecListAppId`, - queryKey: `ExecListAppId`, - waiting: true, - }); useEffect(() => { if ( startSetupApi.data && @@ -237,66 +233,7 @@ export const CippDeploymentStep = (props) => { )} {values.selectedOption === "UpdateTokens" && ( - - Update Tokens - - {appId.isLoading ? ( - - ) : ( - - - - )} - - - } - CardButton={ - <> - - - {!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( - appId?.data?.applicationId - ) && ( - - The Application ID is not valid. Please return to the first page of the SAM - wizard and use the Manual . - - )} - - } - > - - Click the button below to refresh your token. - - {formControl.setValue("noSubmitButton", true)} - - + )} {values.selectedOption === "Manual" && ( diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js new file mode 100644 index 000000000000..099fcef8a39a --- /dev/null +++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material"; +import { CheckCircle } from "@mui/icons-material"; +import CippButtonCard from "../CippCards/CippButtonCard"; +import { ApiGetCall } from "../../api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; + +export const CIPPDeploymentUpdateTokens = ({ formControl }) => { + const values = formControl.getValues(); + const [tokens, setTokens] = useState(null); + + // Get application ID information for the card header + const appId = ApiGetCall({ + url: `/api/ExecListAppId`, + queryKey: `ExecListAppId`, + waiting: true, + }); + + // Handle successful authentication + const handleAuthSuccess = (tokenData) => { + setTokens(tokenData); + console.log("Token data received:", tokenData); + }; + + return ( + + Update Tokens (MSAL Style) + + {appId.isLoading ? ( + + ) : ( + + + + )} + + + } + CardButton={ + + } + > + + Click the button to refresh the Graph token for your tenants. We should write some text here + for replacing token for partner tenant vs client tenant. + + {formControl.setValue("noSubmitButton", true)} + + + ); +}; + +export default CIPPDeploymentUpdateTokens; diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js new file mode 100644 index 000000000000..4de309be07ae --- /dev/null +++ b/src/pages/onboardingv2.js @@ -0,0 +1,107 @@ +import { Layout as DashboardLayout } from "../layouts/index.js"; +import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.js"; +import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.js"; +import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx"; +import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx"; +import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline"; + +const Page = () => { + const steps = [ + { + title: "Step 1", + description: "Onboarding", + component: CippWizardOptionsList, + componentProps: { + title: "Select your setup method", + subtext: `This wizard will guide you through setting up CIPPs access to your client tenants. If this is your first time setting up CIPP you will want to choose the option "Create application for me and connect to my tenants",`, + valuesKey: "SyncTool", + options: [ + { + description: + "Choose this option if this is your first setup, or if you'd like to redo the previous setup.", + icon: , + label: "First Setup", + value: "FirstSetup", + }, + { + description: + "Choose this option if you would like to add a tenant to your environment.", + icon: , + label: "Add a tenant", + value: "AddTenant", + }, + { + description: + "Choose this option if you want to setup which application registration is used to connect to your tenants.", + icon: , + label: "Create a new application registration for me and connect to my tenants", + value: "CreateApp", + }, + { + description: "I would like to refresh my token or replace the account I've used.", + icon: , + label: "Refresh Tokens for existing application registration", + value: "UpdateTokens", + }, + { + description: + "I have an existing application and would like to manually enter my token, or update them. This is only recommended for advanced users.", + icon: , + label: "Manually enter credentials", + value: "Manual", + }, + ], + }, + }, + { + title: "Step 2", + description: "Application", + component: CippDeploymentStep, + }, + { + title: "Step 3", + description: "Tenants", + component: CippDeploymentStep, + }, + { + title: "Step 4", + description: "Baselines", + component: CippDeploymentStep, + }, + { + title: "Step 5", + description: "Integrations", + component: CippDeploymentStep, + }, + { + title: "Step 6", + description: "Notifications", + component: CippDeploymentStep, + }, + { + title: "Step 7", + description: "Alerts", + component: CippDeploymentStep, + }, + { + title: "Step 8", + description: "Confirmation", + component: CippWizardConfirmation, + }, + ]; + + return ( + <> + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 1a50daf3164e59da7274f8ee5cdfa527c0a3c3b4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 16 May 2025 02:42:03 +0200 Subject: [PATCH 033/143] Experimentation with new MSAL and creation --- .../CippComponents/CIPPDeviceCodeButton.js | 253 ++++++++++++++ .../CippComponents/CIPPM365OAuthButton.js | 326 +++++++++++++----- .../CippWizard/CIPPDeploymentUpdateTokens.js | 90 +++-- 3 files changed, 566 insertions(+), 103 deletions(-) create mode 100644 src/components/CippComponents/CIPPDeviceCodeButton.js diff --git a/src/components/CippComponents/CIPPDeviceCodeButton.js b/src/components/CippComponents/CIPPDeviceCodeButton.js new file mode 100644 index 000000000000..e262b69c7912 --- /dev/null +++ b/src/components/CippComponents/CIPPDeviceCodeButton.js @@ -0,0 +1,253 @@ +import { useState, useEffect } from "react"; +import { + Alert, + Button, + Stack, + Typography, + CircularProgress, + Box, +} from "@mui/material"; +import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; + +/** + * CIPPDeviceCodeButton - A button component for Microsoft 365 OAuth authentication using device code flow + * + * @param {Object} props - Component props + * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data + * @param {Function} props.onAuthError - Callback function called when authentication fails with error data + * @param {string} props.buttonText - Text to display on the button (default: "Login with Device Code") + * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true) + * @returns {JSX.Element} The CIPPDeviceCodeButton component + */ +export const CIPPDeviceCodeButton = ({ + onAuthSuccess, + onAuthError, + buttonText = "Login with Device Code", + showResults = true, +}) => { + const [authInProgress, setAuthInProgress] = useState(false); + const [authError, setAuthError] = useState(null); + const [deviceCodeInfo, setDeviceCodeInfo] = useState(null); + const [currentStep, setCurrentStep] = useState(0); + const [pollInterval, setPollInterval] = useState(null); + const [tokens, setTokens] = useState({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + // Get application ID information from API + const appIdInfo = ApiGetCall({ + url: `/api/ExecListAppId`, + queryKey: `ExecListAppId`, + waiting: true, + }); + + // Handle closing the error + const handleCloseError = () => { + setAuthError(null); + }; + + // Clear polling interval when component unmounts + useEffect(() => { + return () => { + if (pollInterval) { + clearInterval(pollInterval); + } + }; + }, [pollInterval]); + + // Start device code authentication + const startDeviceCodeAuth = async () => { + try { + setAuthInProgress(true); + setAuthError(null); + setDeviceCodeInfo(null); + setCurrentStep(1); + + // Call the API to start device code flow + const response = await fetch(`/api/ExecSAMSetup?CreateSAM=true`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (response.ok && data.code) { + // Store device code info + setDeviceCodeInfo({ + user_code: data.code, + verification_uri: data.url, + expires_in: 900, // Default to 15 minutes if not provided + }); + + // Start polling for token + const interval = setInterval(checkAuthStatus, 5000); + setPollInterval(interval); + } else { + // Error getting device code + setAuthError({ + errorCode: "device_code_error", + errorMessage: data.message || "Failed to get device code", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError(error); + } + } catch (error) { + console.error("Error starting device code authentication:", error); + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred during device code authentication", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError(error); + } + }; + + // Check authentication status + const checkAuthStatus = async () => { + try { + // Call the API to check auth status + const response = await fetch(`/api/ExecSAMSetup?CheckSetupProcess=true&step=1`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (response.ok) { + if (data.step === 2) { + // Authentication successful + clearInterval(pollInterval); + setPollInterval(null); + + // Process token data + const tokenData = { + accessToken: "Successfully authenticated", + refreshToken: "Token stored on server", + accessTokenExpiresOn: new Date(Date.now() + 3600 * 1000), // 1 hour from now + refreshTokenExpiresOn: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now + username: "authenticated user", + tenantId: data.tenantId || "unknown", + onmicrosoftDomain: null, + }; + + // Store tokens in component state + setTokens(tokenData); + setDeviceCodeInfo(null); + setCurrentStep(2); + + // Call the onAuthSuccess callback if provided + if (onAuthSuccess) onAuthSuccess(tokenData); + + // Update UI state + setAuthInProgress(false); + } + } else { + // Error checking auth status + clearInterval(pollInterval); + setPollInterval(null); + + setAuthError({ + errorCode: "auth_status_error", + errorMessage: data.message || "Failed to check authentication status", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError({ + errorCode: "auth_status_error", + errorMessage: data.message || "Failed to check authentication status", + timestamp: new Date().toISOString(), + }); + } + } catch (error) { + console.error("Error checking auth status:", error); + // Don't stop polling on transient errors + } + }; + + return ( +
+ + + {!appIdInfo.isLoading && + !appIdInfo?.data?.applicationId && ( + + The Application ID is not valid. Please check your configuration. + + ) + } + + {showResults && ( + + {deviceCodeInfo && authInProgress ? ( + + Device Code Authentication + + To sign in, use a web browser to open the page {deviceCodeInfo.verification_uri} and enter the code {deviceCodeInfo.user_code} to authenticate. + + + Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes + + + ) : tokens.accessToken ? ( + + Authentication Successful + + You've successfully refreshed your token using device code flow. + + {tokens.tenantId && ( + + Tenant ID: {tokens.tenantId} + + )} + + ) : authError ? ( + + Authentication Error: {authError.errorCode} + {authError.errorMessage} + + Time: {authError.timestamp} + + + + + + ) : null} + + )} +
+ ); +}; + +export default CIPPDeviceCodeButton; \ No newline at end of file diff --git a/src/components/CippComponents/CIPPM365OAuthButton.js b/src/components/CippComponents/CIPPM365OAuthButton.js index 9d162aba8971..9c1e3ae58675 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.js +++ b/src/components/CippComponents/CIPPM365OAuthButton.js @@ -8,16 +8,19 @@ import { Box, } from "@mui/material"; import { ApiGetCall } from "../../api/ApiCall"; +import { CippCopyToClipBoard } from "./CippCopyToClipboard"; /** * CIPPM365OAuthButton - A reusable button component for Microsoft 365 OAuth authentication - * + * * @param {Object} props - Component props * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data * @param {Function} props.onAuthError - Callback function called when authentication fails with error data * @param {string} props.buttonText - Text to display on the button (default: "Login with Microsoft") * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true) * @param {string} props.scope - OAuth scope to request (default: "https://graph.microsoft.com/.default offline_access profile openid") + * @param {boolean} props.useDeviceCode - Whether to use device code flow instead of popup (default: false) + * @param {string} props.applicationId - Application ID to use for authentication (default: uses the one from API) * @returns {JSX.Element} The CIPPM365OAuthButton component */ export const CIPPM365OAuthButton = ({ @@ -26,9 +29,12 @@ export const CIPPM365OAuthButton = ({ buttonText = "Login with Microsoft", showResults = true, scope = "https://graph.microsoft.com/.default offline_access profile openid", + useDeviceCode = false, + applicationId = null, }) => { const [authInProgress, setAuthInProgress] = useState(false); const [authError, setAuthError] = useState(null); + const [deviceCodeInfo, setDeviceCodeInfo] = useState(null); const [tokens, setTokens] = useState({ accessToken: null, refreshToken: null, @@ -39,8 +45,8 @@ export const CIPPM365OAuthButton = ({ onmicrosoftDomain: null, }); - // Get application ID information - const appId = ApiGetCall({ + // Get application ID information from API if not provided + const appIdInfo = ApiGetCall({ url: `/api/ExecListAppId`, queryKey: `ExecListAppId`, waiting: true, @@ -51,6 +57,216 @@ export const CIPPM365OAuthButton = ({ setAuthError(null); }; + // Device code authentication function + const handleDeviceCodeAuthentication = async () => { + setAuthInProgress(true); + setAuthError(null); + setDeviceCodeInfo(null); + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + try { + // Get the application ID to use + const appId = applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + + // Request device code from our API endpoint + const deviceCodeResponse = await fetch(`/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent(scope)}`); + const deviceCodeData = await deviceCodeResponse.json(); + + if (deviceCodeResponse.ok && deviceCodeData.user_code) { + // Store device code info + setDeviceCodeInfo(deviceCodeData); + + // Open popup to device login page + const width = 500; + const height = 600; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + "https://microsoft.com/devicelogin", + "deviceLoginPopup", + `width=${width},height=${height},left=${left},top=${top}` + ); + + // Start polling for token + const pollInterval = deviceCodeData.interval || 5; + const expiresIn = deviceCodeData.expires_in || 900; + const startTime = Date.now(); + + const pollForToken = async () => { + // Check if popup was closed + if (popup && popup.closed) { + clearInterval(checkPopupClosed); + setAuthError({ + errorCode: "user_cancelled", + errorMessage: "Authentication was cancelled. Please try again.", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + return; + } + + // Check if we've exceeded the expiration time + if (Date.now() - startTime >= expiresIn * 1000) { + if (popup && !popup.closed) { + popup.close(); + } + setAuthError({ + errorCode: "timeout", + errorMessage: "Device code authentication timed out", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + return; + } + + try { + // Poll for token using our API endpoint + const tokenResponse = await fetch(`/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeData.device_code}`); + const tokenData = await tokenResponse.json(); + + if (tokenResponse.ok && tokenData.status === "success") { + // Successfully got token + if (popup && !popup.closed) { + popup.close(); + } + handleTokenResponse(tokenData); + } else if (tokenData.error === 'authorization_pending' || tokenData.status === "pending") { + // User hasn't completed authentication yet, continue polling + setTimeout(pollForToken, pollInterval * 1000); + } else if (tokenData.error === 'slow_down') { + // Server asking us to slow down polling + setTimeout(pollForToken, (pollInterval + 5) * 1000); + } else { + // Other error + if (popup && !popup.closed) { + popup.close(); + } + setAuthError({ + errorCode: tokenData.error || "token_error", + errorMessage: tokenData.error_description || "Failed to get token", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + } + } catch (error) { + console.error("Error polling for token:", error); + setTimeout(pollForToken, pollInterval * 1000); + } + }; + + // Also monitor for popup closing as a fallback + const checkPopupClosed = setInterval(() => { + if (popup && popup.closed) { + clearInterval(checkPopupClosed); + setAuthInProgress(false); + setAuthError({ + errorCode: "user_cancelled", + errorMessage: "Authentication was cancelled. Please try again.", + timestamp: new Date().toISOString(), + }); + } + }, 1000); + + // Start polling + setTimeout(pollForToken, pollInterval * 1000); + } else { + // Error getting device code + setAuthError({ + errorCode: deviceCodeData.error || "device_code_error", + errorMessage: deviceCodeData.error_description || "Failed to get device code", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + } + } catch (error) { + console.error("Error in device code authentication:", error); + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred during device code authentication", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + } + }; + + // Process token response (common for both auth methods) + const handleTokenResponse = (tokenData) => { + // Extract token information + const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000); + // Refresh tokens typically last for 90 days, but this can vary + const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); + + // Extract information from ID token if available + let username = "unknown user"; + let tenantId = "unknown tenant"; + let onmicrosoftDomain = null; + + if (tokenData.id_token) { + try { + const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1])); + + // Extract username + username = + idTokenPayload.preferred_username || + idTokenPayload.email || + idTokenPayload.upn || + idTokenPayload.name || + "unknown user"; + + // Extract tenant ID if available in the token + if (idTokenPayload.tid) { + tenantId = idTokenPayload.tid; + } + + // Try to extract onmicrosoft domain from the username or issuer + if (username && username.includes("@") && username.includes(".onmicrosoft.com")) { + onmicrosoftDomain = username.split("@")[1]; + } else if (idTokenPayload.iss) { + const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//); + if (issuerMatch && issuerMatch[1]) { + // We have the tenant ID, but not the domain name + } + } + } catch (error) { + console.error("Error parsing ID token:", error); + } + } + + // Create token result object + const tokenResult = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + accessTokenExpiresOn: accessTokenExpiresOn, + refreshTokenExpiresOn: refreshTokenExpiresOn, + username: username, + tenantId: tenantId, + onmicrosoftDomain: onmicrosoftDomain, + }; + + // Store tokens in component state + setTokens(tokenResult); + setDeviceCodeInfo(null); + + // Log only the necessary token information to console + console.log("Access Token:", tokenData.access_token); + console.log("Refresh Token:", tokenData.refresh_token); + + // Call the onAuthSuccess callback if provided + if (onAuthSuccess) onAuthSuccess(tokenResult); + + // Update UI state + setAuthInProgress(false); + }; + // MSAL-like authentication function const handleMsalAuthentication = () => { // Clear previous authentication state when starting a new authentication @@ -66,10 +282,13 @@ export const CIPPM365OAuthButton = ({ onmicrosoftDomain: null, }); + // Get the application ID to use + const appId = applicationId || appIdInfo?.data?.applicationId; + // Generate MSAL-like authentication parameters const msalConfig = { auth: { - clientId: appId?.data?.applicationId, + clientId: appId, authority: `https://login.microsoftonline.com/common`, redirectUri: window.location.origin, }, @@ -109,7 +328,7 @@ export const CIPPM365OAuthButton = ({ // Create the auth URL with PKCE parameters const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + - `client_id=${appId?.data?.applicationId}` + + `client_id=${appId}` + `&response_type=code` + `&redirect_uri=${encodeURIComponent(window.location.origin)}` + `&scope=${encodeURIComponent(scope)}` + @@ -158,7 +377,7 @@ export const CIPPM365OAuthButton = ({ // Prepare the token request const tokenRequest = { grant_type: "authorization_code", - client_id: appId?.data?.applicationId, + client_id: appId, code: code, redirect_uri: window.location.origin, code_verifier: codeVerifier, @@ -180,69 +399,7 @@ export const CIPPM365OAuthButton = ({ const tokenData = await tokenResponse.json(); if (tokenResponse.ok) { - // Extract token information - const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000); - // Refresh tokens typically last for 90 days, but this can vary - // For demonstration, we'll set it to 90 days from now - const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); - - // Extract information from ID token if available - let username = "unknown user"; - let tenantId = appId?.data?.tenantId || "unknown tenant"; - let onmicrosoftDomain = null; - - if (tokenData.id_token) { - try { - const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1])); - - // Extract username - username = - idTokenPayload.preferred_username || - idTokenPayload.email || - idTokenPayload.upn || - idTokenPayload.name || - "unknown user"; - - // Extract tenant ID if available in the token - if (idTokenPayload.tid) { - tenantId = idTokenPayload.tid; - } - - // Try to extract onmicrosoft domain from the username or issuer - if (username && username.includes("@") && username.includes(".onmicrosoft.com")) { - onmicrosoftDomain = username.split("@")[1]; - } else if (idTokenPayload.iss) { - const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//); - if (issuerMatch && issuerMatch[1]) { - // We have the tenant ID, but not the domain name - // We could potentially make an API call to get the domain, but for now we'll leave it null - } - } - } catch (error) { - console.error("Error parsing ID token:", error); - } - } - - // Create token result object - const tokenResult = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - accessTokenExpiresOn: accessTokenExpiresOn, - refreshTokenExpiresOn: refreshTokenExpiresOn, - username: username, - tenantId: tenantId, - onmicrosoftDomain: onmicrosoftDomain, - }; - - // Store tokens in component state - setTokens(tokenResult); - - // Log only the necessary token information to console - console.log("Access Token:", tokenData.access_token); - console.log("Refresh Token:", tokenData.refresh_token); - - // Call the onAuthSuccess callback if provided - if (onAuthSuccess) onAuthSuccess(tokenResult); + handleTokenResponse(tokenData); } else { // Handle token error - display in error box instead of throwing const error = { @@ -398,13 +555,13 @@ export const CIPPM365OAuthButton = ({ - {!appId.isLoading && + {!applicationId && !appIdInfo.isLoading && !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( - appId?.data?.applicationId + appIdInfo?.data?.applicationId ) && ( The Application ID is not valid. Please check your configuration. @@ -429,7 +586,22 @@ export const CIPPM365OAuthButton = ({ {showResults && ( - {tokens.accessToken ? ( + {deviceCodeInfo && authInProgress ? ( + + Device Code Authentication + + A popup window has been opened to microsoft.com/devicelogin. + Enter this code to authenticate: + + + If the popup was blocked or you closed it, you can also go to microsoft.com/devicelogin manually + and enter the code shown above. + + + Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes + + + ) : tokens.accessToken ? ( Authentication Successful diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js index 099fcef8a39a..14f57cf99202 100644 --- a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js +++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js @@ -24,33 +24,71 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => { }; return ( - - Update Tokens (MSAL Style) - - {appId.isLoading ? ( - - ) : ( - - - - )} + + + Update Tokens (MSAL Style) + + {appId.isLoading ? ( + + ) : ( + + + + )} + - - } - CardButton={ - - } - > - - Click the button to refresh the Graph token for your tenants. We should write some text here - for replacing token for partner tenant vs client tenant. - - {formControl.setValue("noSubmitButton", true)} - - + } + CardButton={ + + } + > + + Click the button to refresh the Graph token for your tenants using popup authentication. + This method opens a popup window where you can sign in to your Microsoft account. + + {formControl.setValue("noSubmitButton", true)} + + + + + Update Tokens (Device Code Flow) + + {appId.isLoading ? ( + + ) : ( + + + + )} + + + } + CardButton={ + + } + > + + Click the button to refresh the Graph token using Device Code Flow. This will open a popup + to microsoft.com/devicelogin where you can enter the provided code to authenticate. This + method is useful when regular popup authentication fails or when you need to authenticate + from a different device than the one running CIPP. + + + ); }; From e9775ca35de6142bd30576652ccf690be83e4e0a Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 16 May 2025 16:21:38 +0800 Subject: [PATCH 034/143] Fixes view of filtered standards when 3 or less results returned --- src/components/CippStandards/CippStandardDialog.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index cb9f571218c3..5bc741800718 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -59,6 +59,7 @@ const CippStandardDialog = ({ open={dialogOpen} onClose={handleCloseDialog} maxWidth="xxl" + fullWidth PaperProps={{ sx: { minWidth: "720px", From 838098bc06afd46e64f37aa492002ef6a010ba6e Mon Sep 17 00:00:00 2001 From: Esco Date: Fri, 16 May 2025 10:41:32 +0200 Subject: [PATCH 035/143] fix: Skype Consumer Interoperability with Teams is no longer supported --- src/data/standards.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 3a75fdc58266..6af613f0a7eb 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -3377,11 +3377,6 @@ "name": "standards.TeamsExternalAccessPolicy.EnableFederationAccess", "label": "Allow communication from trusted organizations" }, - { - "type": "switch", - "name": "standards.TeamsExternalAccessPolicy.EnablePublicCloudAccess", - "label": "Allow user to communicate with Skype users" - }, { "type": "switch", "name": "standards.TeamsExternalAccessPolicy.EnableTeamsConsumerAccess", @@ -3407,11 +3402,6 @@ "name": "standards.TeamsFederationConfiguration.AllowTeamsConsumer", "label": "Allow users to communicate with other organizations" }, - { - "type": "switch", - "name": "standards.TeamsFederationConfiguration.AllowPublicUsers", - "label": "Allow users to communicate with Skype Users" - }, { "type": "autoComplete", "required": true, From 6b8f38c432f9087e4089708e99b04b0b007a96db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 16 May 2025 19:11:39 +0200 Subject: [PATCH 036/143] Set 'creatable' to false for some autoCompletes --- .../tenant/administration/alert-configuration/alert.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index 41f09ddd481b..6cff52e95ff7 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -399,6 +399,7 @@ const AlertWizard = () => { type="autoComplete" name="logbook" multiple={false} + creatable={false} formControl={formControl} validators={{ required: { value: true, message: "This field is required" }, @@ -503,7 +504,8 @@ const AlertWizard = () => { required: { value: true, message: "This field is required" }, }} formControl={formControl} - multiple + multiple={true} + creatable={false} options={actionstoTake} /> @@ -574,6 +576,7 @@ const AlertWizard = () => { type="autoComplete" validators={{ required: true }} multiple={false} + creatable={false} name="command" formControl={formControl} label="What alerting script should run" @@ -588,6 +591,7 @@ const AlertWizard = () => { { required: { value: true, message: "This field is required" }, }} formControl={formControl} - multiple + multiple={true} + creatable={false} options={postExecutionOptions} /> From dac4c8766b1b871db657c27640d19a4570f9ba55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 16 May 2025 19:12:34 +0200 Subject: [PATCH 037/143] Feat: Add EntraConnectSyncStatus alert --- src/data/alerts.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/data/alerts.json b/src/data/alerts.json index 4779d23e58e1..e71be4e1547f 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -28,6 +28,15 @@ "inputName": "InactiveLicensedUsersExcludeDisabled", "recommendedRunInterval": "1d" }, + { + "name": "EntraConnectSyncStatus", + "label": "Alert if Entra Connect sync is enabled and has not run in the last X hours", + "requiresInput": true, + "inputType": "number", + "inputLabel": "Hours(Default:72)", + "inputName": "EntraConnectSyncStatusHours", + "recommendedRunInterval": "1d" + }, { "name": "QuotaUsed", "label": "Alert on % mailbox quota used", From c11fc3d0c2856c3487be78547a5afd89603182fb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 May 2025 16:02:58 -0400 Subject: [PATCH 038/143] add psa test option --- src/pages/cipp/settings/notifications.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/cipp/settings/notifications.js b/src/pages/cipp/settings/notifications.js index b9fc1cd25aa6..4c693d10cbd9 100644 --- a/src/pages/cipp/settings/notifications.js +++ b/src/pages/cipp/settings/notifications.js @@ -171,6 +171,11 @@ const Page = () => { name: "sendWebhookNow", label: "Send Webhook Now", }, + { + type: "switch", + name: "sendPsaNow", + label: "Send to PSA Now", + }, ]} api={{ confirmText: From 790dd320625724837855a4434c2d01dad91be0da Mon Sep 17 00:00:00 2001 From: ngms-psh Date: Fri, 16 May 2025 22:24:11 +0200 Subject: [PATCH 039/143] Added standard for Custom Quarantine Policies --- src/data/standards.json | 112 ++++++++++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 15 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 3a75fdc58266..8e97645da4d2 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -1871,6 +1871,7 @@ { "type": "select", "multiple": false, + "creatable": true, "label": "Quarantine policy for Spoof", "name": "standards.AntiPhishPolicy.SpoofQuarantineTag", "options": [ @@ -1911,6 +1912,7 @@ { "type": "select", "multiple": false, + "creatable": true, "label": "Quarantine policy for user impersonation", "name": "standards.AntiPhishPolicy.TargetedUserQuarantineTag", "options": [ @@ -1951,6 +1953,7 @@ { "type": "select", "multiple": false, + "creatable": true, "label": "Quarantine policy for domain impersonation", "name": "standards.AntiPhishPolicy.TargetedDomainQuarantineTag", "options": [ @@ -1991,6 +1994,7 @@ { "type": "select", "multiple": false, + "creatable": true, "label": "Apply quarantine policy", "name": "standards.AntiPhishPolicy.MailboxIntelligenceQuarantineTag", "options": [ @@ -2045,6 +2049,7 @@ { "type": "select", "multiple": false, + "creatable": true, "label": "QuarantineTag", "name": "standards.SafeAttachmentPolicy.QuarantineTag", "options": [ @@ -2171,6 +2176,7 @@ { "type": "select", "multiple": false, + "creatable": true, "label": "QuarantineTag", "name": "standards.MalwareFilterPolicy.QuarantineTag", "options": [ @@ -2276,7 +2282,7 @@ "type": "autoComplete", "required": true, "multiple": false, - "creatable": false, + "creatable": true, "label": "Spam Quarantine Tag", "name": "standards.SpamFilterPolicy.SpamQuarantineTag", "options": [ @@ -2316,7 +2322,7 @@ "type": "autoComplete", "required": true, "multiple": false, - "creatable": false, + "creatable": true, "label": "High Confidence Spam Quarantine Tag", "name": "standards.SpamFilterPolicy.HighConfidenceSpamQuarantineTag", "options": [ @@ -2356,7 +2362,7 @@ "type": "autoComplete", "required": true, "multiple": false, - "creatable": false, + "creatable": true, "label": "Bulk Quarantine Tag", "name": "standards.SpamFilterPolicy.BulkQuarantineTag", "options": [ @@ -2396,7 +2402,7 @@ "type": "autoComplete", "required": true, "multiple": false, - "creatable": false, + "creatable": true, "label": "Phish Quarantine Tag", "name": "standards.SpamFilterPolicy.PhishQuarantineTag", "options": [ @@ -2418,7 +2424,7 @@ "type": "autoComplete", "required": true, "multiple": false, - "creatable": false, + "creatable": true, "label": "High Confidence Phish Quarantine Tag", "name": "standards.SpamFilterPolicy.HighConfidencePhishQuarantineTag", "options": [ @@ -2527,6 +2533,92 @@ "addedDate": "2024-07-15", "powershellEquivalent": "New-HostedContentFilterPolicy or Set-HostedContentFilterPolicy", "recommendedBy": [] + }, + { + "name": "standards.QuarantineTemplate", + "cat": "Defender Standards", + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + }, + "tag": [], + "helpText": "This standard creates a Custom Quarantine Policies that can be used in Anti-Spam and all MDO365 policies. Quarantine Policies can be used to specify recipients permissions, enable end-user spam notifications, and specify the release action preference", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": true, + "name": "displayName", + "label": "Quarantine Display Name", + "required": true + }, + { + "type": "switch", + "label": "Enable end-user spam notifications", + "name": "ESNEnabled", + "defaultValue": true, + "required": false + }, + { + "type": "select", + "multiple": false, + "label": "Select release action preference", + "name": "ReleaseAction", + "options": [ + { + "label": "Allow recipients to request a message to be released from quarantine", + "value": "PermissionToRequestRelease" + }, + { + "label": "Allow recipients to release a message from quarantine", + "value": "PermissionToRelease" + } + ] + }, + { + "type": "switch", + "label": "Include Messages From Blocked Sender Address", + "name": "IncludeMessagesFromBlockedSenderAddress", + "defaultValue": false, + "required": false + }, + { + "type": "switch", + "label": "Allow recipients to delete message", + "name": "PermissionToDelete", + "defaultValue": false, + "required": false + }, + { + "type": "switch", + "label": "Allow recipients to preview message", + "name": "PermissionToPreview", + "defaultValue": false, + "required": false + }, + { + "type": "switch", + "label": "Allow recipients to block Sender Address", + "name": "PermissionToBlockSender", + "defaultValue": false, + "required": false + }, + { + "type": "switch", + "label": "Allow recipients to whitelist Sender Address", + "name": "PermissionToAllowSender", + "defaultValue": false, + "required": false + } + ], + "label": "Custom Quarantine Policy", + "multiple": true, + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-05-16", + "powershellEquivalent": "Set-QuarantinePolicy or New-QuarantinePolicy", + "recommendedBy": [] }, { "name": "standards.intuneDeviceRetirementDays", @@ -3377,11 +3469,6 @@ "name": "standards.TeamsExternalAccessPolicy.EnableFederationAccess", "label": "Allow communication from trusted organizations" }, - { - "type": "switch", - "name": "standards.TeamsExternalAccessPolicy.EnablePublicCloudAccess", - "label": "Allow user to communicate with Skype users" - }, { "type": "switch", "name": "standards.TeamsExternalAccessPolicy.EnableTeamsConsumerAccess", @@ -3407,11 +3494,6 @@ "name": "standards.TeamsFederationConfiguration.AllowTeamsConsumer", "label": "Allow users to communicate with other organizations" }, - { - "type": "switch", - "name": "standards.TeamsFederationConfiguration.AllowPublicUsers", - "label": "Allow users to communicate with Skype Users" - }, { "type": "autoComplete", "required": true, From 911532734b2cff04d418987388d85a4cf14a99c2 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Fri, 16 May 2025 21:53:58 +0100 Subject: [PATCH 040/143] feat: Enhance CippExchangeSettingsForm with dynamic permission handling and tooltip for private item visibility --- .../CippExchangeSettingsForm.jsx | 102 ++++++++++++------ 1 file changed, 70 insertions(+), 32 deletions(-) diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index db924f6476d1..1b293aefd7d1 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Box, Button, @@ -9,6 +9,7 @@ import { Stack, SvgIcon, Typography, + Tooltip, } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -18,6 +19,7 @@ import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; import { Grid } from "@mui/system"; import { CippApiResults } from "../CippComponents/CippApiResults"; +import { useWatch, useFormContext } from "react-hook-form"; const CippExchangeSettingsForm = (props) => { const userSettingsDefaults = useSettings(); @@ -312,44 +314,80 @@ const CippExchangeSettingsForm = (props) => { multiple={false} formControl={formControl} /> - - - value ? true : "Select the permission level for the calendar", - }} - isFetching={isFetching || usersList.isFetching} - options={[ - { value: "Author", label: "Author" }, - { value: "Contributor", label: "Contributor" }, - { value: "Editor", label: "Editor" }, - { value: "Owner", label: "Owner" }, - { value: "NonEditingAuthor", label: "Non Editing Author" }, - { value: "PublishingAuthor", label: "Publishing Author" }, - { value: "PublishingEditor", label: "Publishing Editor" }, - { value: "Reviewer", label: "Reviewer" }, - { value: "LimitedDetails", label: "Limited Details" }, - { value: "AvailabilityOnly", label: "Availability Only" }, - ]} - multiple={false} - formControl={formControl} - /> + + + value ? true : "Select the permission level for the calendar", + }} + isFetching={isFetching || usersList.isFetching} + options={[ + { value: "Author", label: "Author" }, + { value: "Contributor", label: "Contributor" }, + { value: "Editor", label: "Editor" }, + { value: "Owner", label: "Owner" }, + { value: "NonEditingAuthor", label: "Non Editing Author" }, + { value: "PublishingAuthor", label: "Publishing Author" }, + { value: "PublishingEditor", label: "Publishing Editor" }, + { value: "Reviewer", label: "Reviewer" }, + { value: "LimitedDetails", label: "Limited Details" }, + { value: "AvailabilityOnly", label: "Availability Only" }, + ]} + multiple={false} + formControl={formControl} + /> + + {(() => { + const permissionLevel = useWatch({ + control: formControl.control, + name: "calendar.Permissions" + }); + const isEditor = permissionLevel?.value === "Editor"; + + // Use useEffect to handle the switch value reset + useEffect(() => { + if (!isEditor) { + formControl.setValue("calendar.CanViewPrivateItems", false); + } + }, [isEditor, formControl]); + + return ( + + + + + + ); + })()} + + + From 5a261ae31b3d0be4c9e2d0b571f281cfd96a33c7 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 17 May 2025 12:58:24 +0200 Subject: [PATCH 041/143] first new wizard step --- .../CippComponents/CIPPM365OAuthButton.js | 144 +++++++++--------- .../CippComponents/CippApiResults.jsx | 6 +- .../CippComponents/CippCopyToClipboard.jsx | 92 ++++++----- .../CippWizard/CIPPDeploymentUpdateTokens.js | 5 +- src/components/CippWizard/CippSAMDeploy.js | 130 ++++++++++++++++ src/pages/onboardingv2.js | 16 +- 6 files changed, 272 insertions(+), 121 deletions(-) create mode 100644 src/components/CippWizard/CippSAMDeploy.js diff --git a/src/components/CippComponents/CIPPM365OAuthButton.js b/src/components/CippComponents/CIPPM365OAuthButton.js index 9c1e3ae58675..f562c275ea50 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.js +++ b/src/components/CippComponents/CIPPM365OAuthButton.js @@ -1,33 +1,14 @@ import { useState } from "react"; -import { - Alert, - Button, - Stack, - Typography, - CircularProgress, - Box, -} from "@mui/material"; +import { Alert, Button, Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material"; import { ApiGetCall } from "../../api/ApiCall"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; -/** - * CIPPM365OAuthButton - A reusable button component for Microsoft 365 OAuth authentication - * - * @param {Object} props - Component props - * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data - * @param {Function} props.onAuthError - Callback function called when authentication fails with error data - * @param {string} props.buttonText - Text to display on the button (default: "Login with Microsoft") - * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true) - * @param {string} props.scope - OAuth scope to request (default: "https://graph.microsoft.com/.default offline_access profile openid") - * @param {boolean} props.useDeviceCode - Whether to use device code flow instead of popup (default: false) - * @param {string} props.applicationId - Application ID to use for authentication (default: uses the one from API) - * @returns {JSX.Element} The CIPPM365OAuthButton component - */ export const CIPPM365OAuthButton = ({ onAuthSuccess, onAuthError, buttonText = "Login with Microsoft", showResults = true, + showSuccessAlert = true, scope = "https://graph.microsoft.com/.default offline_access profile openid", useDeviceCode = false, applicationId = null, @@ -74,33 +55,38 @@ export const CIPPM365OAuthButton = ({ try { // Get the application ID to use - const appId = applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID - + const appId = + applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + // Request device code from our API endpoint - const deviceCodeResponse = await fetch(`/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent(scope)}`); + const deviceCodeResponse = await fetch( + `/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent( + scope + )}` + ); const deviceCodeData = await deviceCodeResponse.json(); - + if (deviceCodeResponse.ok && deviceCodeData.user_code) { // Store device code info setDeviceCodeInfo(deviceCodeData); - + // Open popup to device login page const width = 500; const height = 600; const left = window.screen.width / 2 - width / 2; const top = window.screen.height / 2 - height / 2; - + const popup = window.open( "https://microsoft.com/devicelogin", "deviceLoginPopup", `width=${width},height=${height},left=${left},top=${top}` ); - + // Start polling for token const pollInterval = deviceCodeData.interval || 5; const expiresIn = deviceCodeData.expires_in || 900; const startTime = Date.now(); - + const pollForToken = async () => { // Check if popup was closed if (popup && popup.closed) { @@ -113,7 +99,7 @@ export const CIPPM365OAuthButton = ({ setAuthInProgress(false); return; } - + // Check if we've exceeded the expiration time if (Date.now() - startTime >= expiresIn * 1000) { if (popup && !popup.closed) { @@ -127,22 +113,27 @@ export const CIPPM365OAuthButton = ({ setAuthInProgress(false); return; } - + try { // Poll for token using our API endpoint - const tokenResponse = await fetch(`/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeData.device_code}`); + const tokenResponse = await fetch( + `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeData.device_code}` + ); const tokenData = await tokenResponse.json(); - + if (tokenResponse.ok && tokenData.status === "success") { // Successfully got token if (popup && !popup.closed) { popup.close(); } handleTokenResponse(tokenData); - } else if (tokenData.error === 'authorization_pending' || tokenData.status === "pending") { + } else if ( + tokenData.error === "authorization_pending" || + tokenData.status === "pending" + ) { // User hasn't completed authentication yet, continue polling setTimeout(pollForToken, pollInterval * 1000); - } else if (tokenData.error === 'slow_down') { + } else if (tokenData.error === "slow_down") { // Server asking us to slow down polling setTimeout(pollForToken, (pollInterval + 5) * 1000); } else { @@ -162,7 +153,7 @@ export const CIPPM365OAuthButton = ({ setTimeout(pollForToken, pollInterval * 1000); } }; - + // Also monitor for popup closing as a fallback const checkPopupClosed = setInterval(() => { if (popup && popup.closed) { @@ -175,7 +166,7 @@ export const CIPPM365OAuthButton = ({ }); } }, 1000); - + // Start polling setTimeout(pollForToken, pollInterval * 1000); } else { @@ -209,11 +200,11 @@ export const CIPPM365OAuthButton = ({ let username = "unknown user"; let tenantId = "unknown tenant"; let onmicrosoftDomain = null; - + if (tokenData.id_token) { try { const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1])); - + // Extract username username = idTokenPayload.preferred_username || @@ -221,12 +212,12 @@ export const CIPPM365OAuthButton = ({ idTokenPayload.upn || idTokenPayload.name || "unknown user"; - + // Extract tenant ID if available in the token if (idTokenPayload.tid) { tenantId = idTokenPayload.tid; } - + // Try to extract onmicrosoft domain from the username or issuer if (username && username.includes("@") && username.includes(".onmicrosoft.com")) { onmicrosoftDomain = username.split("@")[1]; @@ -262,7 +253,7 @@ export const CIPPM365OAuthButton = ({ // Call the onAuthSuccess callback if provided if (onAuthSuccess) onAuthSuccess(tokenResult); - + // Update UI state setAuthInProgress(false); }; @@ -448,7 +439,7 @@ export const CIPPM365OAuthButton = ({ }; setAuthError(error); if (onAuthError) onAuthError(error); - + // Ensure we're not showing any previous success state setTokens({ accessToken: null, @@ -532,7 +523,7 @@ export const CIPPM365OAuthButton = ({ }; setAuthError(error); if (onAuthError) onAuthError(error); - + // Ensure we're not showing any previous success state setTokens({ accessToken: null, @@ -557,9 +548,10 @@ export const CIPPM365OAuthButton = ({ disabled={ appIdInfo.isLoading || authInProgress || - (!applicationId && !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( - appIdInfo?.data?.applicationId - )) + (!applicationId && + !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( + appIdInfo?.data?.applicationId + )) } onClick={useDeviceCode ? handleDeviceCodeAuthentication : handleMsalAuthentication} color="primary" @@ -574,15 +566,15 @@ export const CIPPM365OAuthButton = ({ )} - {!applicationId && !appIdInfo.isLoading && + {!applicationId && + !appIdInfo.isLoading && !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( appIdInfo?.data?.applicationId ) && ( The Application ID is not valid. Please check your configuration. - ) - } + )} {showResults && ( @@ -590,37 +582,45 @@ export const CIPPM365OAuthButton = ({ Device Code Authentication - A popup window has been opened to microsoft.com/devicelogin. - Enter this code to authenticate: + A popup window has been opened to microsoft.com/devicelogin. Enter + this code to authenticate:{" "} + - If the popup was blocked or you closed it, you can also go to microsoft.com/devicelogin manually - and enter the code shown above. + If the popup was blocked or you closed it, you can also go to{" "} + microsoft.com/devicelogin manually and enter the code shown above. Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes ) : tokens.accessToken ? ( - - Authentication Successful - - You've successfully refreshed your token. The account you're using for authentication - is: {tokens.username} - - - Tenant ID: {tokens.tenantId} - {tokens.onmicrosoftDomain && ( - <> | Domain: {tokens.onmicrosoftDomain} - )} - - - Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()} - - + showSuccessAlert ? ( + + Authentication Successful + + You've successfully refreshed your token. The account you're using for + authentication is: {tokens.username} + + + Tenant ID: {tokens.tenantId} + {tokens.onmicrosoftDomain && ( + <> + {" "} + | Domain: {tokens.onmicrosoftDomain} + + )} + + + Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()} + + + ) : null ) : authError ? ( - Authentication Error: {authError.errorCode} + + Authentication Error: {authError.errorCode} + {authError.errorMessage} Time: {authError.timestamp} @@ -637,5 +637,3 @@ export const CIPPM365OAuthButton = ({ ); }; - -export default CIPPM365OAuthButton; \ No newline at end of file diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx index 21917507c396..b351ec4dd80b 100644 --- a/src/components/CippComponents/CippApiResults.jsx +++ b/src/components/CippComponents/CippApiResults.jsx @@ -73,7 +73,7 @@ const extractAllResults = (data) => { results.push(processed); } } else { - const ignoreKeys = ["metadata", "Metadata"]; + const ignoreKeys = ["metadata", "Metadata", "severity"]; if (typeof obj === "object") { Object.keys(obj).forEach((key) => { @@ -296,7 +296,9 @@ export const CippApiResults = (props) => { ))} )} - {(apiObject.isSuccess || apiObject.isError) && finalResults?.length > 0 ? ( + {(apiObject.isSuccess || apiObject.isError) && + finalResults?.length > 0 && + hasVisibleResults ? ( tableDialog.handleOpen()}> diff --git a/src/components/CippComponents/CippCopyToClipboard.jsx b/src/components/CippComponents/CippCopyToClipboard.jsx index 8f2cefd686a4..6d3603790df7 100644 --- a/src/components/CippComponents/CippCopyToClipboard.jsx +++ b/src/components/CippComponents/CippCopyToClipboard.jsx @@ -4,58 +4,68 @@ import { useState } from "react"; import CopyToClipboard from "react-copy-to-clipboard"; export const CippCopyToClipBoard = (props) => { - const { text, type = "button", ...other } = props; + const { text, type = "button", visible = true, ...other } = props; const [showPassword, setShowPassword] = useState(false); + const handleTogglePassword = () => { setShowPassword((prev) => !prev); }; - return ( - <> - {type === "button" && ( - - - - - - - - - - )} - {type === "chip" && ( - + + if (!visible) return null; + + if (type === "button") { + return ( + + + + + + + + + + ); + } + + if (type === "chip") { + return ( + + + + + + ); + } + + if (type === "password") { + return ( + <> + + + {showPassword ? : } + + + - )} - {type === "password" && ( - <> - - - {showPassword ? : } - - - - - - - - - )} - - ); + + ); + } + + return null; }; diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js index 14f57cf99202..fd70341ab548 100644 --- a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js +++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js @@ -82,10 +82,7 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => { } > - Click the button to refresh the Graph token using Device Code Flow. This will open a popup - to microsoft.com/devicelogin where you can enter the provided code to authenticate. This - method is useful when regular popup authentication fails or when you need to authenticate - from a different device than the one running CIPP. + Device code flow test diff --git a/src/components/CippWizard/CippSAMDeploy.js b/src/components/CippWizard/CippSAMDeploy.js new file mode 100644 index 000000000000..e45fa2a9f87b --- /dev/null +++ b/src/components/CippWizard/CippSAMDeploy.js @@ -0,0 +1,130 @@ +import { useState } from "react"; +import { Alert, Stack, Box, CircularProgress } from "@mui/material"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; + +export const CippSAMDeploy = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props; + const [authStatus, setAuthStatus] = useState({ + success: false, + error: null, + loading: false, + }); + + //TODO: Make sure to block next button until the app is created. + + // API call to create/update SAM app + const createSamApp = ApiPostCall({ urlfromdata: true }); + // Handle successful authentication + const handleAuthSuccess = (tokenData) => { + setAuthStatus({ + success: false, + error: null, + loading: true, + }); + + // Send the access token to the API to create/update SAM app + createSamApp.mutate({ + url: "/api/ExecCreateSamApp", + data: { access_token: tokenData.accessToken }, + }); + }; + + // Handle authentication error + const handleAuthError = (error) => { + setAuthStatus({ + success: false, + error: error.errorMessage || "Authentication failed", + loading: false, + }); + }; + + // Update status when API call completes + if (createSamApp.isSuccess && authStatus.loading) { + const data = createSamApp.data; + if (data.severity === "error") { + setAuthStatus({ + success: false, + error: data.message || "Failed to create SAM application", + loading: false, + }); + } else if (data.severity === "success") { + setAuthStatus({ + success: true, + error: null, + loading: false, + }); + // Allow user to proceed to next step + formControl.setValue("samAppCreated", true); + } + } + + // Handle API error + if (createSamApp.isError && authStatus.loading) { + setAuthStatus({ + success: false, + error: "An error occurred while creating the SAM application", + loading: false, + }); + } + + return ( + + + This step will create or update the CIPP Application Registration in your tenant. Make sure + the account you use is one of the following roles: +
    +
  • Global Administrator or Privileged Role Administrator
  • +
  • Application Administrator
  • +
  • Cloud Application Administrator
  • +
+
+ + {/* Show API results */} + + + {/* Show error message if any */} + {authStatus.error && ( + + {authStatus.error} + + )} + + {/* Show success message when authentication is successful */} + {authStatus.success && !authStatus.loading && ( + + SAM application has been successfully created/updated. You can now proceed to the next + step. + + )} + + {/* Show authenticate button only if not successful yet */} + {(!authStatus.success || authStatus.loading) && ( + + + + + + )} + + +
+ ); +}; + +export default CippSAMDeploy; diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js index 4de309be07ae..f2cb811cedc7 100644 --- a/src/pages/onboardingv2.js +++ b/src/pages/onboardingv2.js @@ -3,6 +3,7 @@ import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfi import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.js"; import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx"; import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx"; +import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.js"; import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline"; const Page = () => { @@ -56,37 +57,50 @@ const Page = () => { { title: "Step 2", description: "Application", - component: CippDeploymentStep, + component: CippSAMDeploy, + componentProps: { + title: "SAM Application Setup", + subtext: "This step will create or update the SAM application in your tenant.", + }, }, { title: "Step 3", description: "Tenants", component: CippDeploymentStep, + //set the tenant mode to "GDAP", "perTenant" or "mixed". + //if the tenant mode is set to GDAP, show MSAL button to update token. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "GDAP" } + //if the tenant mode is set to perTenant, show MSAL button to get token. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "perTenant" }. List each tenant that has authenticated and been added. + //if the tenant mode is set to mixed, show first MSAL button to update GDAP access, then show second MSAL button to update perTenant access. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "mixed" } }, { title: "Step 4", description: "Baselines", component: CippDeploymentStep, + //give choice to download baselines from repos. }, { title: "Step 5", description: "Integrations", component: CippDeploymentStep, + //give the choice to configure integrations. }, { title: "Step 6", description: "Notifications", component: CippDeploymentStep, + //explain notifications, test if email is setup,etc. }, { title: "Step 7", description: "Alerts", component: CippDeploymentStep, + //show template alerts, allow user to configure them. }, { title: "Step 8", description: "Confirmation", component: CippWizardConfirmation, + //confirm and finish button, perform tasks, launch checks etc. }, ]; From 4204310110067c357f9002c45fd771053a759aa6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 18 May 2025 01:11:45 +0200 Subject: [PATCH 042/143] updates to new sam wizard --- ...OAuthButton.js => CIPPM365OAuthButton.jsx} | 547 +++++++++++------- ...ploymentStep.js => CIPPDeploymentStep.jsx} | 0 ...kens.js => CIPPDeploymentUpdateTokens.jsx} | 1 + ...ialsStep.js => CippPSACredentialsStep.jsx} | 0 ...ASyncOptions.js => CippPSASyncOptions.jsx} | 0 .../{CippSAMDeploy.js => CippSAMDeploy.jsx} | 27 +- .../CippWizard/CippTenantModeDeploy.jsx | 326 +++++++++++ ...irmation.js => CippWizardConfirmation.jsx} | 0 src/pages/authredirect.js | 46 ++ src/pages/onboardingv2.js | 17 +- .../tenant/administration/tenants/add.js | 2 +- 11 files changed, 721 insertions(+), 245 deletions(-) rename src/components/CippComponents/{CIPPM365OAuthButton.js => CIPPM365OAuthButton.jsx} (58%) rename src/components/CippWizard/{CIPPDeploymentStep.js => CIPPDeploymentStep.jsx} (100%) rename src/components/CippWizard/{CIPPDeploymentUpdateTokens.js => CIPPDeploymentUpdateTokens.jsx} (98%) rename src/components/CippWizard/{CippPSACredentialsStep.js => CippPSACredentialsStep.jsx} (100%) rename src/components/CippWizard/{CippPSASyncOptions.js => CippPSASyncOptions.jsx} (100%) rename src/components/CippWizard/{CippSAMDeploy.js => CippSAMDeploy.jsx} (82%) create mode 100644 src/components/CippWizard/CippTenantModeDeploy.jsx rename src/components/CippWizard/{CippWizardConfirmation.js => CippWizardConfirmation.jsx} (100%) create mode 100644 src/pages/authredirect.js diff --git a/src/components/CippComponents/CIPPM365OAuthButton.js b/src/components/CippComponents/CIPPM365OAuthButton.jsx similarity index 58% rename from src/components/CippComponents/CIPPM365OAuthButton.js rename to src/components/CippComponents/CIPPM365OAuthButton.jsx index f562c275ea50..6241823034c7 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.js +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -1,5 +1,5 @@ -import { useState } from "react"; -import { Alert, Button, Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material"; +import { useState, useEffect } from "react"; +import { Alert, Button, Typography, CircularProgress, Box } from "@mui/material"; import { ApiGetCall } from "../../api/ApiCall"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; @@ -12,10 +12,14 @@ export const CIPPM365OAuthButton = ({ scope = "https://graph.microsoft.com/.default offline_access profile openid", useDeviceCode = false, applicationId = null, + autoStartDeviceLogon = false, + validateServiceAccount = true, // Add prop to control service account validation }) => { const [authInProgress, setAuthInProgress] = useState(false); const [authError, setAuthError] = useState(null); const [deviceCodeInfo, setDeviceCodeInfo] = useState(null); + const [codeRetrievalInProgress, setCodeRetrievalInProgress] = useState(false); + const [isServiceAccount, setIsServiceAccount] = useState(true); // Default to true to avoid showing warning initially const [tokens, setTokens] = useState({ accessToken: null, refreshToken: null, @@ -29,29 +33,35 @@ export const CIPPM365OAuthButton = ({ // Get application ID information from API if not provided const appIdInfo = ApiGetCall({ url: `/api/ExecListAppId`, - queryKey: `ExecListAppId`, waiting: true, }); + // Ensure appId is refetched every time the component is mounted + useEffect(() => { + // Refetch appId when component mounts + appIdInfo.refetch(); + }, []); // Empty dependency array ensures this runs only on mount + // Handle closing the error const handleCloseError = () => { setAuthError(null); }; - // Device code authentication function - const handleDeviceCodeAuthentication = async () => { - setAuthInProgress(true); + // Check if username is a service account (contains "service" or "cipp") + const checkIsServiceAccount = (username) => { + if (!username || !validateServiceAccount) return true; // If no username or validation disabled, don't show warning + + const lowerUsername = username.toLowerCase(); + return lowerUsername.includes("service") || lowerUsername.includes("cipp"); + }; + + // Function to retrieve device code + const retrieveDeviceCode = async () => { + setCodeRetrievalInProgress(true); setAuthError(null); - setDeviceCodeInfo(null); - setTokens({ - accessToken: null, - refreshToken: null, - accessTokenExpiresOn: null, - refreshTokenExpiresOn: null, - username: null, - tenantId: null, - onmicrosoftDomain: null, - }); + + // Refetch appId to ensure we have the latest + await appIdInfo.refetch(); try { // Get the application ID to use @@ -69,117 +79,151 @@ export const CIPPM365OAuthButton = ({ if (deviceCodeResponse.ok && deviceCodeData.user_code) { // Store device code info setDeviceCodeInfo(deviceCodeData); + } else { + // Error getting device code + setAuthError({ + errorCode: deviceCodeData.error || "device_code_error", + errorMessage: deviceCodeData.error_description || "Failed to get device code", + timestamp: new Date().toISOString(), + }); + } + } catch (error) { + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred retrieving device code", + timestamp: new Date().toISOString(), + }); + } finally { + setCodeRetrievalInProgress(false); + } + }; - // Open popup to device login page - const width = 500; - const height = 600; - const left = window.screen.width / 2 - width / 2; - const top = window.screen.height / 2 - height / 2; - - const popup = window.open( - "https://microsoft.com/devicelogin", - "deviceLoginPopup", - `width=${width},height=${height},left=${left},top=${top}` - ); - - // Start polling for token - const pollInterval = deviceCodeData.interval || 5; - const expiresIn = deviceCodeData.expires_in || 900; - const startTime = Date.now(); - - const pollForToken = async () => { - // Check if popup was closed - if (popup && popup.closed) { - clearInterval(checkPopupClosed); - setAuthError({ - errorCode: "user_cancelled", - errorMessage: "Authentication was cancelled. Please try again.", - timestamp: new Date().toISOString(), - }); - setAuthInProgress(false); - return; + // Device code authentication function - opens popup and starts polling + const handleDeviceCodeAuthentication = async () => { + // Refetch appId to ensure we have the latest + await appIdInfo.refetch(); + + if (!deviceCodeInfo) { + // If we don't have a device code yet, retrieve it first + await retrieveDeviceCode(); + return; + } + + setAuthInProgress(true); + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + try { + // Get the application ID to use - refetch already happened at the start of this function + const appId = + applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + + // Open popup to device login page + const width = 500; + const height = 600; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + "https://microsoft.com/devicelogin", + "deviceLoginPopup", + `width=${width},height=${height},left=${left},top=${top}` + ); + + // Start polling for token + const pollInterval = deviceCodeInfo.interval || 5; + const expiresIn = deviceCodeInfo.expires_in || 900; + const startTime = Date.now(); + + const pollForToken = async () => { + // Check if popup was closed + if (popup && popup.closed) { + clearInterval(checkPopupClosed); + setAuthError({ + errorCode: "user_cancelled", + errorMessage: "Authentication was cancelled. Please try again.", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + return; + } + + // Check if we've exceeded the expiration time + if (Date.now() - startTime >= expiresIn * 1000) { + if (popup && !popup.closed) { + popup.close(); } + setAuthError({ + errorCode: "timeout", + errorMessage: "Device code authentication timed out", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + return; + } + + try { + // Poll for token using our API endpoint + const tokenResponse = await fetch( + `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}` + ); + const tokenData = await tokenResponse.json(); - // Check if we've exceeded the expiration time - if (Date.now() - startTime >= expiresIn * 1000) { + if (tokenResponse.ok && tokenData.status === "success") { + // Successfully got token + if (popup && !popup.closed) { + popup.close(); + } + handleTokenResponse(tokenData); + } else if ( + tokenData.error === "authorization_pending" || + tokenData.status === "pending" + ) { + // User hasn't completed authentication yet, continue polling + setTimeout(pollForToken, pollInterval * 1000); + } else if (tokenData.error === "slow_down") { + // Server asking us to slow down polling + setTimeout(pollForToken, (pollInterval + 5) * 1000); + } else { + // Other error if (popup && !popup.closed) { popup.close(); } setAuthError({ - errorCode: "timeout", - errorMessage: "Device code authentication timed out", + errorCode: tokenData.error || "token_error", + errorMessage: tokenData.error_description || "Failed to get token", timestamp: new Date().toISOString(), }); setAuthInProgress(false); - return; - } - - try { - // Poll for token using our API endpoint - const tokenResponse = await fetch( - `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeData.device_code}` - ); - const tokenData = await tokenResponse.json(); - - if (tokenResponse.ok && tokenData.status === "success") { - // Successfully got token - if (popup && !popup.closed) { - popup.close(); - } - handleTokenResponse(tokenData); - } else if ( - tokenData.error === "authorization_pending" || - tokenData.status === "pending" - ) { - // User hasn't completed authentication yet, continue polling - setTimeout(pollForToken, pollInterval * 1000); - } else if (tokenData.error === "slow_down") { - // Server asking us to slow down polling - setTimeout(pollForToken, (pollInterval + 5) * 1000); - } else { - // Other error - if (popup && !popup.closed) { - popup.close(); - } - setAuthError({ - errorCode: tokenData.error || "token_error", - errorMessage: tokenData.error_description || "Failed to get token", - timestamp: new Date().toISOString(), - }); - setAuthInProgress(false); - } - } catch (error) { - console.error("Error polling for token:", error); - setTimeout(pollForToken, pollInterval * 1000); } - }; + } catch (error) { + setTimeout(pollForToken, pollInterval * 1000); + } + }; - // Also monitor for popup closing as a fallback - const checkPopupClosed = setInterval(() => { - if (popup && popup.closed) { - clearInterval(checkPopupClosed); - setAuthInProgress(false); - setAuthError({ - errorCode: "user_cancelled", - errorMessage: "Authentication was cancelled. Please try again.", - timestamp: new Date().toISOString(), - }); - } - }, 1000); + // Also monitor for popup closing as a fallback + const checkPopupClosed = setInterval(() => { + if (popup && popup.closed) { + clearInterval(checkPopupClosed); + setAuthInProgress(false); + setAuthError({ + errorCode: "user_cancelled", + errorMessage: "Authentication was cancelled. Please try again.", + timestamp: new Date().toISOString(), + }); + } + }, 1000); - // Start polling - setTimeout(pollForToken, pollInterval * 1000); - } else { - // Error getting device code - setAuthError({ - errorCode: deviceCodeData.error || "device_code_error", - errorMessage: deviceCodeData.error_description || "Failed to get device code", - timestamp: new Date().toISOString(), - }); - setAuthInProgress(false); - } + // Start polling + setTimeout(pollForToken, pollInterval * 1000); } catch (error) { - console.error("Error in device code authentication:", error); setAuthError({ errorCode: "device_code_error", errorMessage: error.message || "An error occurred during device code authentication", @@ -227,9 +271,10 @@ export const CIPPM365OAuthButton = ({ // We have the tenant ID, but not the domain name } } - } catch (error) { - console.error("Error parsing ID token:", error); - } + + // Check if username is a service account + setIsServiceAccount(checkIsServiceAccount(username)); + } catch (error) {} } // Create token result object @@ -243,15 +288,9 @@ export const CIPPM365OAuthButton = ({ onmicrosoftDomain: onmicrosoftDomain, }; - // Store tokens in component state setTokens(tokenResult); setDeviceCodeInfo(null); - // Log only the necessary token information to console - console.log("Access Token:", tokenData.access_token); - console.log("Refresh Token:", tokenData.refresh_token); - - // Call the onAuthSuccess callback if provided if (onAuthSuccess) onAuthSuccess(tokenResult); // Update UI state @@ -259,7 +298,7 @@ export const CIPPM365OAuthButton = ({ }; // MSAL-like authentication function - const handleMsalAuthentication = () => { + const handleMsalAuthentication = async () => { // Clear previous authentication state when starting a new authentication setAuthInProgress(true); setAuthError(null); @@ -273,7 +312,10 @@ export const CIPPM365OAuthButton = ({ onmicrosoftDomain: null, }); - // Get the application ID to use + // Refetch app ID info to ensure we have the latest + await appIdInfo.refetch(); + + // Get the application ID to use - now we're sure to have the latest after the await const appId = applicationId || appIdInfo?.data?.applicationId; // Generate MSAL-like authentication parameters @@ -281,7 +323,7 @@ export const CIPPM365OAuthButton = ({ auth: { clientId: appId, authority: `https://login.microsoftonline.com/common`, - redirectUri: window.location.origin, + redirectUri: `${window.location.origin}/authredirect`, }, }; @@ -290,9 +332,6 @@ export const CIPPM365OAuthButton = ({ scopes: [scope], }; - console.log("MSAL Config:", msalConfig); - console.log("Login Request:", loginRequest); - // Generate PKCE code verifier and challenge const generateCodeVerifier = () => { const array = new Uint8Array(32); @@ -300,36 +339,20 @@ export const CIPPM365OAuthButton = ({ return Array.from(array, (byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)).join(""); }; - const base64URLEncode = (str) => { - return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); - }; - - // Generate code verifier for PKCE const codeVerifier = generateCodeVerifier(); - // In a real implementation, we would hash the code verifier to create the code challenge - // For simplicity, we'll use the same value const codeChallenge = codeVerifier; - - // Note: We're not storing the code verifier in session storage for security reasons - // Instead, we'll use it directly in the token exchange - - // Create a random state value for security const state = Math.random().toString(36).substring(2, 15); - - // Create the auth URL with PKCE parameters const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + `client_id=${appId}` + `&response_type=code` + - `&redirect_uri=${encodeURIComponent(window.location.origin)}` + + `&redirect_uri=${encodeURIComponent(window.location.origin)}/authredirect` + `&scope=${encodeURIComponent(scope)}` + `&code_challenge=${codeChallenge}` + `&code_challenge_method=plain` + `&state=${state}` + `&prompt=select_account`; - console.log("MSAL Auth URL:", authUrl); - // Open popup for authentication const width = 500; const height = 600; @@ -347,7 +370,6 @@ export const CIPPM365OAuthButton = ({ // Verify the state parameter matches what we sent (security check) if (receivedState !== state) { const errorMessage = "State mismatch in auth response - possible CSRF attack"; - console.error(errorMessage); const error = { errorCode: "state_mismatch", errorMessage: errorMessage, @@ -358,38 +380,70 @@ export const CIPPM365OAuthButton = ({ setAuthInProgress(false); return; } - - console.log("Authorization code received:", code); - try { - // Actually exchange the code for tokens using the token endpoint - console.log("Exchanging authorization code for tokens..."); - // Prepare the token request const tokenRequest = { grant_type: "authorization_code", client_id: appId, code: code, - redirect_uri: window.location.origin, + redirect_uri: `${window.location.origin}/authredirect`, code_verifier: codeVerifier, }; - // Make the token request - const tokenResponse = await fetch( - `https://login.microsoftonline.com/common/oauth2/v2.0/token`, - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams(tokenRequest).toString(), - } - ); + // Make the token request through our API proxy to avoid origin header issues + const tokenResponse = await fetch(`/api/ExecTokenExchange`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + tokenRequest, + tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", + tenantId: appId, // Pass the tenant ID to retrieve the correct client secret + }), + }); // Parse the token response const tokenData = await tokenResponse.json(); + // Check if the response contains an error + if (tokenData.error) { + const error = { + errorCode: tokenData.error || "token_error", + errorMessage: + tokenData.error_description || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + setAuthInProgress(false); + return; + } + if (tokenResponse.ok) { + // If we have a refresh token, store it + if (tokenData.refresh_token) { + try { + // Store the refresh token + const refreshResponse = await fetch(`/api/ExecUpdateRefreshToken`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + tenantId: appId, + refreshtoken: tokenData.refresh_token, + }), + }); + + if (!refreshResponse.ok) { + console.warn("Failed to store refresh token, but continuing with authentication"); + } + } catch (error) { + console.error("Failed to store refresh token:", error); + } + } + handleTokenResponse(tokenData); } else { // Handle token error - display in error box instead of throwing @@ -403,7 +457,6 @@ export const CIPPM365OAuthButton = ({ if (onAuthError) onAuthError(error); } } catch (error) { - console.error("Error exchanging code for tokens:", error); const errorObj = { errorCode: "token_exchange_error", errorMessage: error.message || "Failed to exchange authorization code for tokens", @@ -431,7 +484,6 @@ export const CIPPM365OAuthButton = ({ // If authentication is still in progress when popup closes, it's an error if (authInProgress) { const errorMessage = "Authentication was cancelled. Please try again."; - console.error(errorMessage); const error = { errorCode: "user_cancelled", errorMessage: errorMessage, @@ -463,9 +515,6 @@ export const CIPPM365OAuthButton = ({ // Check if the URL contains a code parameter (authorization code) if (currentUrl.includes("code=") && currentUrl.includes("state=")) { clearInterval(checkPopupLocation); - - console.log("Detected authorization code in URL:", currentUrl); - // Parse the URL to extract the code and state const urlParams = new URLSearchParams(popup.location.search); const code = urlParams.get("code"); @@ -478,9 +527,6 @@ export const CIPPM365OAuthButton = ({ // Check for error in the URL if (currentUrl.includes("error=")) { clearInterval(checkPopupLocation); - - console.error("Detected error in authentication response:", currentUrl); - // Parse the URL to extract the error details const urlParams = new URLSearchParams(popup.location.search); const errorCode = urlParams.get("error"); @@ -515,7 +561,6 @@ export const CIPPM365OAuthButton = ({ // If authentication is still in progress when popup closes, it's an error if (authInProgress) { const errorMessage = "Authentication was cancelled. Please try again."; - console.error(errorMessage); const error = { errorCode: "user_cancelled", errorMessage: errorMessage, @@ -541,33 +586,32 @@ export const CIPPM365OAuthButton = ({ }, 1000); }; + // Auto-start device code retrieval if requested + useEffect(() => { + if ( + useDeviceCode && + autoStartDeviceLogon && + !codeRetrievalInProgress && + !deviceCodeInfo && + !tokens.accessToken && + appIdInfo?.data + ) { + retrieveDeviceCode(); + } + }, [ + useDeviceCode, + autoStartDeviceLogon, + codeRetrievalInProgress, + deviceCodeInfo, + tokens.accessToken, + appIdInfo?.data, + ]); + return (
- - {!applicationId && !appIdInfo.isLoading && + appIdInfo?.data && // Only check if data is available !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( appIdInfo?.data?.applicationId ) && ( @@ -577,45 +621,77 @@ export const CIPPM365OAuthButton = ({ )} {showResults && ( - - {deviceCodeInfo && authInProgress ? ( + + {deviceCodeInfo ? ( Device Code Authentication - A popup window has been opened to microsoft.com/devicelogin. Enter - this code to authenticate:{" "} + {authInProgress ? ( + <> + A popup window has been opened to microsoft.com/devicelogin. + Enter this code to authenticate:{" "} + + ) : ( + <>Click the button below to authenticate. You will need to enter this code: + )} - If the popup was blocked or you closed it, you can also go to{" "} - microsoft.com/devicelogin manually and enter the code shown above. + {authInProgress ? ( + <> + If the popup was blocked or you closed it, you can also go to{" "} + microsoft.com/devicelogin manually and enter the code shown + above. + + ) : ( + <> + When you click the button below, a popup will open to{" "} + microsoft.com/devicelogin where you'll enter this code. + + )} Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes ) : tokens.accessToken ? ( - showSuccessAlert ? ( - - Authentication Successful - - You've successfully refreshed your token. The account you're using for - authentication is: {tokens.username} - - - Tenant ID: {tokens.tenantId} - {tokens.onmicrosoftDomain && ( - <> - {" "} - | Domain: {tokens.onmicrosoftDomain} - - )} - - - Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()} - - - ) : null + <> + {showSuccessAlert ? ( + + Authentication Successful + + You've successfully refreshed your token. The account you're using for + authentication is: {tokens.username} + + + Tenant ID: {tokens.tenantId} + {tokens.onmicrosoftDomain && ( + <> + {" "} + | Domain: {tokens.onmicrosoftDomain} + + )} + + + Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()} + + + ) : null} + + {!isServiceAccount && ( + + Service Account Required + + CIPP requires a service account for authentication. The account you're using ( + {tokens.username}) does not appear to be a service account. + + + Please redo authentication using an account with "service" or "cipp" in the + username. + + + )} + ) : authError ? ( @@ -634,6 +710,31 @@ export const CIPPM365OAuthButton = ({ ) : null} )} +
); }; diff --git a/src/components/CippWizard/CIPPDeploymentStep.js b/src/components/CippWizard/CIPPDeploymentStep.jsx similarity index 100% rename from src/components/CippWizard/CIPPDeploymentStep.js rename to src/components/CippWizard/CIPPDeploymentStep.jsx diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx similarity index 98% rename from src/components/CippWizard/CIPPDeploymentUpdateTokens.js rename to src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx index fd70341ab548..6bfa70481719 100644 --- a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js +++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx @@ -78,6 +78,7 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => { buttonText="Refresh Graph Token (Device Code)" useDeviceCode={true} applicationId="1950a258-227b-4e31-a9cf-717495945fc2" + autoStartDeviceLogon={true} /> } > diff --git a/src/components/CippWizard/CippPSACredentialsStep.js b/src/components/CippWizard/CippPSACredentialsStep.jsx similarity index 100% rename from src/components/CippWizard/CippPSACredentialsStep.js rename to src/components/CippWizard/CippPSACredentialsStep.jsx diff --git a/src/components/CippWizard/CippPSASyncOptions.js b/src/components/CippWizard/CippPSASyncOptions.jsx similarity index 100% rename from src/components/CippWizard/CippPSASyncOptions.js rename to src/components/CippWizard/CippPSASyncOptions.jsx diff --git a/src/components/CippWizard/CippSAMDeploy.js b/src/components/CippWizard/CippSAMDeploy.jsx similarity index 82% rename from src/components/CippWizard/CippSAMDeploy.js rename to src/components/CippWizard/CippSAMDeploy.jsx index e45fa2a9f87b..3aef23bdb69d 100644 --- a/src/components/CippWizard/CippSAMDeploy.js +++ b/src/components/CippWizard/CippSAMDeploy.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Alert, Stack, Box, CircularProgress } from "@mui/material"; +import { Alert, Stack, Box, CircularProgress, Link } from "@mui/material"; import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; import { CippApiResults } from "../CippComponents/CippApiResults"; import { ApiPostCall } from "../../api/ApiCall"; @@ -73,15 +73,23 @@ export const CippSAMDeploy = (props) => { return ( - This step will create or update the CIPP Application Registration in your tenant. Make sure - the account you use is one of the following roles: -
    -
  • Global Administrator or Privileged Role Administrator
  • -
  • Application Administrator
  • -
  • Cloud Application Administrator
  • -
+ To run this setup you will need the following prerequisites: +
  • + A CIPP Service Account. For more information on how to create a service account, click{" "} + + here + +
  • +
  • (Temporary) Global Administrator permissions for the CIPP Service Account
  • +
  • + Multi-factor authentication enabled for the CIPP Service Account, with no trusted + locations or other exclusions. +
  • - {/* Show API results */} @@ -111,6 +119,7 @@ export const CippSAMDeploy = (props) => { useDeviceCode={true} applicationId="1950a258-227b-4e31-a9cf-717495945fc2" showSuccessAlert={false} + autoStartDeviceLogon={true} />
    diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx new file mode 100644 index 000000000000..a478e8ea3b25 --- /dev/null +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -0,0 +1,326 @@ +import { useState, useEffect } from "react"; +import { + Alert, + Stack, + Box, + Typography, + CircularProgress, + Divider, + List, + ListItem, + ListItemText, + Paper, + Switch, + FormControlLabel, +} from "@mui/material"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { ApiPostCall, ApiGetCall } from "../../api/ApiCall"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CippAutoComplete } from "../CippComponents/CippAutocomplete"; + +export const CippTenantModeDeploy = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props; + + const [tenantMode, setTenantMode] = useState("GDAP"); + const [allowPartnerTenantManagement, setAllowPartnerTenantManagement] = useState(false); + const [gdapAuthStatus, setGdapAuthStatus] = useState({ + success: false, + loading: false, + }); + const [perTenantAuthStatus, setPerTenantAuthStatus] = useState({ + success: false, + loading: false, + }); + const [authenticatedTenants, setAuthenticatedTenants] = useState([]); + + // API call to update refresh token + const updateRefreshToken = ApiPostCall({ urlfromdata: true }); + + // API call to get list of authenticated tenants (for perTenant mode) + const tenantList = ApiGetCall({ + url: "/api/ListTenants", + queryKey: "ListTenants", + }); + + // Update authenticated tenants list when tenantList changes + useEffect(() => { + if (tenantList.data && tenantMode === "perTenant") { + setAuthenticatedTenants(tenantList.data); + } + }, [tenantList.data, tenantMode]); + + // Handle tenant mode change + const handleTenantModeChange = (selectedOption) => { + if (selectedOption) { + setTenantMode(selectedOption.value); + // Reset auth status when changing modes + setGdapAuthStatus({ + success: false, + loading: false, + }); + setPerTenantAuthStatus({ + success: false, + loading: false, + }); + // Reset partner tenant management option + setAllowPartnerTenantManagement(false); + } + }; + + // Tenant mode options + const tenantModeOptions = [ + { + label: "GDAP - Uses your partner center to connect to tenants", + value: "GDAP", + }, + { + label: "Per Tenant - Add each tenant individually", + value: "perTenant", + }, + { + label: "Mixed - Use Partner Center and add tenants individually", + value: "mixed", + }, + ]; + + // Handle GDAP authentication success + const handleGdapAuthSuccess = (tokenData) => { + setGdapAuthStatus({ + success: false, + loading: true, + }); + + // Send the refresh token to the API + updateRefreshToken.mutate({ + url: "/api/ExecUpdateRefreshToken", + data: { + tenantId: tokenData.tenantId, + refreshToken: tokenData.refreshToken, + tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode, + allowPartnerTenantManagement: tenantMode === "GDAP" ? allowPartnerTenantManagement : false, + }, + }); + }; + + // Handle perTenant authentication success + const handlePerTenantAuthSuccess = (tokenData) => { + setPerTenantAuthStatus({ + success: false, + loading: true, + }); + + // Send the refresh token to the API + updateRefreshToken.mutate({ + url: "/api/ExecUpdateRefreshToken", + data: { + tenantId: tokenData.tenantId, + refreshToken: tokenData.refreshToken, + tenantMode: tenantMode === "mixed" ? "perTenant" : tenantMode, + }, + }); + }; + + // Update status when API call completes + useEffect(() => { + if (updateRefreshToken.isSuccess) { + const data = updateRefreshToken.data; + + if (data.state === "error") { + if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) { + setGdapAuthStatus({ + success: false, + loading: false, + }); + } else { + setPerTenantAuthStatus({ + success: false, + loading: false, + }); + } + } else if (data.state === "success") { + if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) { + setGdapAuthStatus({ + success: true, + loading: false, + }); + // Allow user to proceed to next step if not in mixed mode + if (tenantMode !== "mixed") { + formControl.setValue("tenantModeSet", true); + } + } else { + setPerTenantAuthStatus({ + success: true, + loading: false, + }); + // Allow user to proceed to next step + formControl.setValue("tenantModeSet", true); + + // Refresh tenant list for perTenant mode + if (tenantMode === "perTenant") { + tenantList.refetch(); + } + } + } + } + }, [updateRefreshToken.isSuccess, updateRefreshToken.data]); + + // Handle API error + useEffect(() => { + if (updateRefreshToken.isError) { + if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) { + setGdapAuthStatus({ + success: false, + loading: false, + }); + } else { + setPerTenantAuthStatus({ + success: false, + loading: false, + }); + } + } + }, [updateRefreshToken.isError]); + + return ( + + + Select how you want to connect to your tenants. You have three options: +
      +
    • + GDAP: Use delegated administration (recommended) +
    • +
    • + Per Tenant: Authenticate to each tenant individually +
    • +
    • + Mixed: Use both GDAP and per-tenant authentication +
    • +
    +
    + + {/* Tenant mode selection */} + + + Tenant Connection Mode + + option.value === tenantMode)} + onChange={handleTenantModeChange} + multiple={false} + required={true} + /> + + + + + {/* Show API results */} + + + {/* GDAP Authentication Section */} + {(tenantMode === "GDAP" || tenantMode === "mixed") && ( + + + GDAP Authentication + + + {/* Show success message when authentication is successful */} + {gdapAuthStatus.success && ( + + GDAP authentication successful. You can now proceed to the next step. + + )} + + {/* GDAP Partner Tenant Management Switch */} + setAllowPartnerTenantManagement(e.target.checked)} + color="primary" + /> + } + label="Allow management of the partner tenant" + /> + + {/* Show authenticate button only if not successful yet */} + {(!gdapAuthStatus.success || gdapAuthStatus.loading) && ( + + + + + + )} + + )} + + {/* Per Tenant Authentication Section */} + {(tenantMode === "perTenant" || (tenantMode === "mixed" && gdapAuthStatus.success)) && ( + + + Per-Tenant Authentication + + + {/* Show success message when authentication is successful */} + {perTenantAuthStatus.success && ( + + Per-tenant authentication successful. You can add another tenant or proceed to the + next step. + + )} + + {/* Show authenticate button */} + + + + {(perTenantAuthStatus.loading || updateRefreshToken.isLoading) && ( + + )} + + + + {/* List authenticated tenants for perTenant mode */} + {tenantMode === "perTenant" && authenticatedTenants.length > 0 && ( + + + Authenticated Tenants + + + + {authenticatedTenants.map((tenant, index) => ( + + + + ))} + + + + )} + + )} + + +
    + ); +}; + +export default CippTenantModeDeploy; diff --git a/src/components/CippWizard/CippWizardConfirmation.js b/src/components/CippWizard/CippWizardConfirmation.jsx similarity index 100% rename from src/components/CippWizard/CippWizardConfirmation.js rename to src/components/CippWizard/CippWizardConfirmation.jsx diff --git a/src/pages/authredirect.js b/src/pages/authredirect.js new file mode 100644 index 000000000000..892b712bdc47 --- /dev/null +++ b/src/pages/authredirect.js @@ -0,0 +1,46 @@ +import { Box, Container, Grid, Stack } from "@mui/material"; +import Head from "next/head"; +import { CippImageCard } from "../components/CippCards/CippImageCard.jsx"; +import { Layout as DashboardLayout } from "../layouts/index.js"; + +const Page = () => ( + <> + + + Authentication complete + + + + + + + + + + + + + + +); + +export default Page; diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js index f2cb811cedc7..ba5890e30d46 100644 --- a/src/pages/onboardingv2.js +++ b/src/pages/onboardingv2.js @@ -1,9 +1,10 @@ import { Layout as DashboardLayout } from "../layouts/index.js"; -import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.js"; -import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.js"; +import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.jsx"; +import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.jsx"; import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx"; import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx"; -import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.js"; +import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.jsx"; +import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDeploy.jsx"; import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline"; const Page = () => { @@ -58,19 +59,11 @@ const Page = () => { title: "Step 2", description: "Application", component: CippSAMDeploy, - componentProps: { - title: "SAM Application Setup", - subtext: "This step will create or update the SAM application in your tenant.", - }, }, { title: "Step 3", description: "Tenants", - component: CippDeploymentStep, - //set the tenant mode to "GDAP", "perTenant" or "mixed". - //if the tenant mode is set to GDAP, show MSAL button to update token. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "GDAP" } - //if the tenant mode is set to perTenant, show MSAL button to get token. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "perTenant" }. List each tenant that has authenticated and been added. - //if the tenant mode is set to mixed, show first MSAL button to update GDAP access, then show second MSAL button to update perTenant access. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "mixed" } + component: CippTenantModeDeploy, }, { title: "Step 4", diff --git a/src/pages/tenant/administration/tenants/add.js b/src/pages/tenant/administration/tenants/add.js index 68235a1135a8..cdfa1b05143d 100644 --- a/src/pages/tenant/administration/tenants/add.js +++ b/src/pages/tenant/administration/tenants/add.js @@ -3,7 +3,7 @@ import CippWizardPage from "../../../../components/CippWizard/CippWizardPage.jsx import { CippWizardOptionsList } from "../../../../components/CippWizard/CippWizardOptionsList.jsx"; import { CippAddTenantForm } from "../../../../components/CippWizard/CippAddTenantForm.jsx"; import { BuildingOfficeIcon, CloudIcon } from "@heroicons/react/24/outline"; -import CippWizardConfirmation from "../../../../components/CippWizard/CippWizardConfirmation.js"; +import CippWizardConfirmation from "../../../../components/CippWizard/CippWizardConfirmation.jsx"; const Page = () => { const steps = [ From 4592916e5175e3e31f72f727221cdff57c247f9b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 18 May 2025 23:22:40 +0200 Subject: [PATCH 043/143] Updates for single tenant mode --- .../CippComponents/CIPPM365OAuthButton.jsx | 2 + .../CippWizard/CippTenantModeDeploy.jsx | 231 +++++++++--------- 2 files changed, 119 insertions(+), 114 deletions(-) diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx index 6241823034c7..83eae0dc7ebe 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.jsx +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -433,6 +433,8 @@ export const CIPPM365OAuthButton = ({ body: JSON.stringify({ tenantId: appId, refreshtoken: tokenData.refresh_token, + tenantMode: tokenData.tenantMode, + allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement, }), }); diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index a478e8ea3b25..93b49e5bb2c5 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -34,8 +34,9 @@ export const CippTenantModeDeploy = (props) => { }); const [authenticatedTenants, setAuthenticatedTenants] = useState([]); - // API call to update refresh token + // API calls const updateRefreshToken = ApiPostCall({ urlfromdata: true }); + const addTenant = ApiPostCall({ urlfromdata: true }); // API call to get list of authenticated tenants (for perTenant mode) const tenantList = ApiGetCall({ @@ -45,7 +46,7 @@ export const CippTenantModeDeploy = (props) => { // Update authenticated tenants list when tenantList changes useEffect(() => { - if (tenantList.data && tenantMode === "perTenant") { + if (tenantList.data && (tenantMode === "perTenant" || tenantMode === "mixed")) { setAuthenticatedTenants(tenantList.data); } }, [tenantList.data, tenantMode]); @@ -87,100 +88,65 @@ export const CippTenantModeDeploy = (props) => { // Handle GDAP authentication success const handleGdapAuthSuccess = (tokenData) => { setGdapAuthStatus({ - success: false, - loading: true, + success: true, + loading: false, }); - // Send the refresh token to the API - updateRefreshToken.mutate({ - url: "/api/ExecUpdateRefreshToken", - data: { - tenantId: tokenData.tenantId, - refreshToken: tokenData.refreshToken, - tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode, - allowPartnerTenantManagement: tenantMode === "GDAP" ? allowPartnerTenantManagement : false, - }, - }); + // Allow user to proceed to next step for GDAP mode + if (tenantMode === "GDAP") { + formControl.setValue("tenantModeSet", true); + } else if (tenantMode === "mixed") { + // For mixed mode, allow proceeding if either authentication is successful + formControl.setValue("tenantModeSet", true); + } }; // Handle perTenant authentication success const handlePerTenantAuthSuccess = (tokenData) => { setPerTenantAuthStatus({ - success: false, - loading: true, - }); - - // Send the refresh token to the API - updateRefreshToken.mutate({ - url: "/api/ExecUpdateRefreshToken", - data: { - tenantId: tokenData.tenantId, - refreshToken: tokenData.refreshToken, - tenantMode: tenantMode === "mixed" ? "perTenant" : tenantMode, - }, + success: true, + loading: false, }); - }; - // Update status when API call completes - useEffect(() => { - if (updateRefreshToken.isSuccess) { - const data = updateRefreshToken.data; + // For perTenant mode or mixed mode with perTenant auth, add the tenant to the cache + if (tenantMode === "perTenant" || tenantMode === "mixed") { + // Call the AddTenant API to add the tenant to the cache with directTenant status + addTenant.mutate({ + url: "/api/ExecAddTenant", + data: { + tenantId: tokenData.tenantId, + }, + }); + } - if (data.state === "error") { - if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) { - setGdapAuthStatus({ - success: false, - loading: false, - }); - } else { - setPerTenantAuthStatus({ - success: false, - loading: false, - }); - } - } else if (data.state === "success") { - if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) { - setGdapAuthStatus({ - success: true, - loading: false, - }); - // Allow user to proceed to next step if not in mixed mode - if (tenantMode !== "mixed") { - formControl.setValue("tenantModeSet", true); - } - } else { - setPerTenantAuthStatus({ - success: true, - loading: false, - }); - // Allow user to proceed to next step - formControl.setValue("tenantModeSet", true); + // Allow user to proceed to next step + formControl.setValue("tenantModeSet", true); - // Refresh tenant list for perTenant mode - if (tenantMode === "perTenant") { - tenantList.refetch(); - } - } - } + // Refresh tenant list for perTenant and mixed modes + if (tenantMode === "perTenant" || tenantMode === "mixed") { + tenantList.refetch(); } - }, [updateRefreshToken.isSuccess, updateRefreshToken.data]); + }; // Handle API error useEffect(() => { - if (updateRefreshToken.isError) { - if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) { - setGdapAuthStatus({ - success: false, - loading: false, - }); - } else { - setPerTenantAuthStatus({ - success: false, - loading: false, - }); - } + if (addTenant.isError) { + setPerTenantAuthStatus({ + success: false, + loading: false, + }); + } + }, [addTenant.isError]); + + // Handle AddTenant API response + useEffect(() => { + if (addTenant.isSuccess) { + console.log("Tenant added to cache successfully:", addTenant.data); + } else if (addTenant.isError) { + console.error("Failed to add tenant to cache:", addTenant.error); } - }, [updateRefreshToken.isError]); + }, [addTenant.isSuccess, addTenant.isError]); + return ( @@ -217,19 +183,31 @@ export const CippTenantModeDeploy = (props) => { {/* Show API results */} - + + {addTenant.isSuccess && ( + + Tenant successfully added to the cache. + + )} + {addTenant.isError && ( + + Failed to add tenant to the cache: {addTenant.error?.message || "Unknown error"} + + )} {/* GDAP Authentication Section */} {(tenantMode === "GDAP" || tenantMode === "mixed") && ( - GDAP Authentication + Partner Tenant {/* Show success message when authentication is successful */} {gdapAuthStatus.success && ( - GDAP authentication successful. You can now proceed to the next step. + {tenantMode === "mixed" + ? "GDAP authentication successful. You can now proceed to the next step or connect to separate tenants below." + : "GDAP authentication successful. You can now proceed to the next step."} )} @@ -242,7 +220,7 @@ export const CippTenantModeDeploy = (props) => { color="primary" /> } - label="Allow management of the partner tenant" + label="Allow management of the partner tenant." /> {/* Show authenticate button only if not successful yet */} @@ -250,8 +228,18 @@ export const CippTenantModeDeploy = (props) => { { + // Add the tenantMode and allowPartnerTenantManagement parameters to the tokenData + const updatedTokenData = { + ...tokenData, + tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode, + allowPartnerTenantManagement: allowPartnerTenantManagement, + }; + handleGdapAuthSuccess(updatedTokenData); + }} + buttonText={ + tenantMode === "mixed" ? "Connect to GDAP" : "Authenticate with Microsoft GDAP" + } showSuccessAlert={false} /> @@ -261,7 +249,7 @@ export const CippTenantModeDeploy = (props) => { )} {/* Per Tenant Authentication Section */} - {(tenantMode === "perTenant" || (tenantMode === "mixed" && gdapAuthStatus.success)) && ( + {(tenantMode === "perTenant" || tenantMode === "mixed") && ( Per-Tenant Authentication @@ -270,45 +258,60 @@ export const CippTenantModeDeploy = (props) => { {/* Show success message when authentication is successful */} {perTenantAuthStatus.success && ( - Per-tenant authentication successful. You can add another tenant or proceed to the - next step. + {tenantMode === "mixed" + ? "Tenant authentication successful. You can add another tenant or proceed to the next step." + : "Per-tenant authentication successful. You can add another tenant or proceed to the next step."} )} + + {tenantMode === "mixed" + ? "Click the button below to connect to individual tenants. You can authenticate to multiple tenants one by one." + : "You can click the button below to authenticate to a tenant. Perform this authentication for every tenant you wish to manage using CIPP."} + {/* Show authenticate button */} { + // Add the tenantMode parameter to the tokenData + const updatedTokenData = { + ...tokenData, + tenantMode: tenantMode === "mixed" ? "perTenant" : tenantMode, + }; + handlePerTenantAuthSuccess(updatedTokenData); + }} + buttonText={ + tenantMode === "mixed" + ? "Connect to Separate Tenants" + : "Authenticate with Microsoft" + } showSuccessAlert={false} /> - {(perTenantAuthStatus.loading || updateRefreshToken.isLoading) && ( - - )} - {/* List authenticated tenants for perTenant mode */} - {tenantMode === "perTenant" && authenticatedTenants.length > 0 && ( - - - Authenticated Tenants - - - - {authenticatedTenants.map((tenant, index) => ( - - - - ))} - - - - )} + {/* List authenticated tenants for perTenant and mixed modes */} + {(tenantMode === "perTenant" || tenantMode === "mixed") && + authenticatedTenants.length > 0 && ( + + + Authenticated Tenants + + + + {authenticatedTenants.map((tenant, index) => ( + + + + ))} + + + + )} )} From 573fc22c5f5a6aedc6c0693a57d217681d7a4595 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 19 May 2025 10:59:51 +0200 Subject: [PATCH 044/143] changes --- src/components/CippWizard/CippTenantModeDeploy.jsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index 93b49e5bb2c5..d0eee14a96e6 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -147,7 +147,6 @@ export const CippTenantModeDeploy = (props) => { } }, [addTenant.isSuccess, addTenant.isError]); - return ( @@ -202,14 +201,7 @@ export const CippTenantModeDeploy = (props) => { Partner Tenant
    - {/* Show success message when authentication is successful */} - {gdapAuthStatus.success && ( - - {tenantMode === "mixed" - ? "GDAP authentication successful. You can now proceed to the next step or connect to separate tenants below." - : "GDAP authentication successful. You can now proceed to the next step."} - - )} + {/* GDAP Partner Tenant Management Switch */} Date: Mon, 19 May 2025 17:06:43 +0800 Subject: [PATCH 045/143] Passthrough default sorting to CippTablePage to use in Tenant Groups List --- src/components/CippComponents/CippTablePage.jsx | 2 ++ src/pages/tenant/administration/tenants/groups/index.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/CippComponents/CippTablePage.jsx b/src/components/CippComponents/CippTablePage.jsx index eb75f6cc1ad1..60a50c974dc3 100644 --- a/src/components/CippComponents/CippTablePage.jsx +++ b/src/components/CippComponents/CippTablePage.jsx @@ -25,6 +25,7 @@ export const CippTablePage = (props) => { tableFilter, tenantInTitle = true, filters, + defaultSorting = [], sx = { flexGrow: 1, py: 4 }, ...other } = props; @@ -65,6 +66,7 @@ export const CippTablePage = (props) => { columnsFromApi={columnsFromApi} offCanvas={offCanvas} filters={tableFilters} + defaultSorting={defaultSorting} initialState={{ columnFilters: filters ? filters.map(filter => ({ id: filter.id || filter.columnId, diff --git a/src/pages/tenant/administration/tenants/groups/index.js b/src/pages/tenant/administration/tenants/groups/index.js index 8d3e4c328ceb..d8d201634969 100644 --- a/src/pages/tenant/administration/tenants/groups/index.js +++ b/src/pages/tenant/administration/tenants/groups/index.js @@ -38,6 +38,7 @@ const Page = () => { queryKey="TenantGroupListPage" apiDataKey="Results" actions={actions} + defaultSorting={[{ id: "Name", desc: false }]} cardButton={ + + + { + setOffcanvasVisible(false); + }} + > + + + {`${cat}.${obj}`} + + + Listed below are the available API endpoints based on permission level, ReadWrite + level includes endpoints under Read. + + {[apiPermissions[cat][obj]].map((permissions, key) => { + var sections = Object.keys(permissions).map((type) => { + var items = []; + for (var api in permissions[type]) { + items.push({ heading: "", content: permissions[type][api] }); + } + return ( + + {type} + + {items.map((item, idx) => ( + + {item.content} + + ))} + + + ); + }); + return sections; + })} + + + + ); + }; + + return ( + <> + + + + {!selectedRole && ( + + )} + {selectedRole && isBaseRole && ["admin", "superadmin"].includes(selectedRole) && ( + }> + This is a highly privileged role and overrides any custom role restrictions. + + )} + {cippApiRoleSelected && ( + + This is the default role for all API clients in the CIPP-API integration. If you + would like different permissions for specific applications, create a role per + application and select it from the CIPP-API integrations page. + + )} + + + {!isBaseRole && ( + <> + + + {allTenantSelected && blockedTenants?.length == 0 && ( + + All tenants selected, no tenant restrictions will be applied unless blocked + tenants are specified. + + )} + + {allTenantSelected && ( + + + + )} + + )} + {apiPermissionFetching && } + {apiPermissionSuccess && ( + <> + API Permissions + {!isBaseRole && ( + + Set All Permissions + + + + + + )} + + <> + {Object.keys(apiPermissions) + .sort() + .map((cat, catIndex) => ( + + }>{cat} + + {Object.keys(apiPermissions[cat]) + .sort() + .map((obj, index) => { + const readOnly = baseRolePermissions?.[cat] ? true : false; + return ( + + + + ); + })} + + + ))} + + + + )} + + + + {selectedEntraGroup && ( + + This role will be assigned to the Entra Group:{" "} + {selectedEntraGroup.label} + + )} + {selectedTenant?.length > 0 && ( + <> +
    Allowed Tenants
    +
      + {selectedTenant.map((tenant, idx) => ( +
    • {tenant?.label}
    • + ))} +
    + + )} + {blockedTenants?.length > 0 && ( + <> +
    Blocked Tenants
    +
      + {blockedTenants.map((tenant, idx) => ( +
    • {tenant?.label}
    • + ))} +
    + + )} + {selectedPermissions && apiPermissionSuccess && ( + <> +
    Selected Permissions
    +
      + {selectedPermissions && + Object.keys(selectedPermissions) + ?.sort() + .map((cat, idx) => ( + <> + {selectedPermissions?.[cat] && + typeof selectedPermissions[cat] === "string" && + !selectedPermissions[cat]?.includes("None") && ( +
    • {selectedPermissions[cat]}
    • + )} + + ))} +
    + + )} +
    +
    + + + + + + + ); +}; + +export default CippRoleAddEdit; diff --git a/src/components/CippSettings/CippRoles.jsx b/src/components/CippSettings/CippRoles.jsx new file mode 100644 index 000000000000..15766897d4f4 --- /dev/null +++ b/src/components/CippSettings/CippRoles.jsx @@ -0,0 +1,115 @@ +import React from "react"; +import { Box, Button, SvgIcon } from "@mui/material"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import NextLink from "next/link"; +import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard"; +import { getCippTranslation } from "../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { Stack } from "@mui/system"; +import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; + +const CippRoles = () => { + const actions = [ + { + label: "Edit", + icon: ( + + + + ), + link: "/cipp/super-admin/cipp-roles/edit?role=[RoleName]", + }, + { + label: "Delete", + icon: ( + + + + ), + confirmText: "Are you sure you want to delete this custom role?", + url: "/api/ExecCustomRole", + type: "POST", + data: { + Action: "Delete", + RoleName: "RoleName", + }, + condition: (row) => row?.Type === "Custom", + relatedQueryKeys: ["customRoleList"], + }, + ]; + + const offCanvas = { + children: (data) => { + const includeProps = ["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"]; + const keys = includeProps.filter((key) => Object.keys(data).includes(key)); + const properties = []; + keys.forEach((key) => { + if (data[key] && data[key].length > 0) { + properties.push({ + label: getCippTranslation(key), + value: getCippFormatting(data[key], key), + }); + } + }); + + if (data["Permissions"] && Object.keys(data["Permissions"]).length > 0) { + properties.push({ + label: "Permissions", + value: ( + + {Object.keys(data["Permissions"]) + .sort() + .map((permission, idx) => ( + + + + ))} + + ), + }); + } + + return ( + + ); + }, + }; + + return ( + + + + + } + component={NextLink} + href="/cipp/super-admin/cipp-roles/add" + > + Add Role + + } + api={{ + url: "/api/ListCustomRole", + }} + queryKey="customRoleTable" + simpleColumns={["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"]} + offCanvas={offCanvas} + /> + + ); +}; + +export default CippRoles; diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 011886bc4499..10e41a3dbb98 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -1,5 +1,6 @@ import { ApiGetCall } from "../api/ApiCall.jsx"; import UnauthenticatedPage from "../pages/unauthenticated.js"; +import LoadingPage from "../pages/loading.js"; export const PrivateRoute = ({ children, routeType }) => { const { @@ -7,27 +8,39 @@ export const PrivateRoute = ({ children, routeType }) => { error, isLoading, } = ApiGetCall({ - url: "/.auth/me", + url: "/api/me", queryKey: "authmecipp", refetchOnWindowFocus: true, + }); + + const session = ApiGetCall({ + url: "/.auth/me", + queryKey: "authmeswa", + refetchOnWindowFocus: true, staleTime: 120000, // 2 minutes }); + // if not logged into swa + if (null === session?.data?.clientPrincipal || session?.data === undefined) { + return ; + } + if (isLoading) { - return "Loading..."; + return ; } let roles = null; - if (null !== profile?.clientPrincipal) { - roles = profile?.clientPrincipal.userRoles; - } else if (null === profile?.clientPrincipal) { + + if (null !== profile?.clientPrincipal && undefined !== profile) { + roles = profile?.clientPrincipal?.userRoles; + } else if (null === profile?.clientPrincipal || undefined === profile) { return ; } if (null === roles) { return ; } else { const blockedRoles = ["anonymous", "authenticated"]; - const userRoles = roles.filter((role) => !blockedRoles.includes(role)); + const userRoles = roles?.filter((role) => !blockedRoles.includes(role)) ?? []; const isAuthenticated = userRoles.length > 0 && !error; const isAdmin = roles.includes("admin"); if (routeType === "admin") { diff --git a/src/data/cipp-roles.json b/src/data/cipp-roles.json new file mode 100644 index 000000000000..f95e32fa18c6 --- /dev/null +++ b/src/data/cipp-roles.json @@ -0,0 +1,23 @@ +{ + "readonly": { + "include": ["*.Read"], + "exclude": ["CIPP.SuperAdmin.*"] + }, + "editor": { + "include": ["*.Read", "*.ReadWrite"], + "exclude": [ + "CIPP.SuperAdmin.*", + "CIPP.Admin.*", + "CIPP.AppSettings.*", + "Tenant.Standards.ReadWrite" + ] + }, + "admin": { + "include": ["*"], + "exclude": ["CIPP.SuperAdmin.*"] + }, + "superadmin": { + "include": ["*"], + "exclude": [] + } +} diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js index ab6b9a11155b..6f16a30e8788 100644 --- a/src/layouts/account-popover.js +++ b/src/layouts/account-popover.js @@ -38,10 +38,8 @@ export const AccountPopover = (props) => { const popover = usePopover(); const orgData = ApiGetCall({ - url: "/.auth/me", + url: "/api/me", queryKey: "authmecipp", - staleTime: 120000, - refetchOnWindowFocus: true, }); const handleLogout = useCallback(async () => { diff --git a/src/layouts/index.js b/src/layouts/index.js index ddd1b2460645..5a6c9d86b701 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -82,13 +82,20 @@ export const Layout = (props) => { const [menuItems, setMenuItems] = useState(nativeMenuItems); const currentTenant = settings?.currentTenant; const currentRole = ApiGetCall({ - url: "/.auth/me", + url: "/api/me", queryKey: "authmecipp", staleTime: 120000, refetchOnWindowFocus: true, }); const [hideSidebar, setHideSidebar] = useState(false); + const swaStatus = ApiGetCall({ + url: "/.auth/me", + queryKey: "authmeswa", + staleTime: 120000, + refetchOnWindowFocus: true, + }); + useEffect(() => { if (currentRole.isSuccess && !currentRole.isFetching) { const userRoles = currentRole.data?.clientPrincipal?.userRoles; @@ -118,8 +125,15 @@ export const Layout = (props) => { const filteredMenu = filterItemsByRole(nativeMenuItems); setMenuItems(filteredMenu); + } else if ( + swaStatus.isLoading || + swaStatus.data?.clientPrincipal === null || + swaStatus.data === undefined || + currentRole.isLoading + ) { + setHideSidebar(true); } - }, [currentRole.isSuccess]); + }, [currentRole.isSuccess, swaStatus.data, swaStatus.isLoading]); const handleNavPin = useCallback(() => { settings.handleUpdate({ @@ -181,11 +195,11 @@ export const Layout = (props) => { }); useEffect(() => { - if (version.isFetched && !alertsAPI.isFetched) { + if (!hideSidebar && version.isFetched && !alertsAPI.isFetched) { alertsAPI.waiting = true; alertsAPI.refetch(); } - }, [version, alertsAPI]); + }, [version, alertsAPI, hideSidebar]); useEffect(() => { if (alertsAPI.isSuccess && !alertsAPI.isFetching) { @@ -238,6 +252,27 @@ export const Layout = (props) => { }} > + + Setup Wizard + + + + + {!setupCompleted && ( + + + + Setup has not been completed. + + + + + )} {(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? ( @@ -255,30 +290,7 @@ export const Layout = (props) => { ) : ( - <> - - Setup Wizard - - - - - {!setupCompleted && ( - - - - Setup has not been completed. - - - - - )} - {children} - + <>{children} )}