I'm Working through this challenge and appear to have run into an issue i have not found a solution to (nor does it appear that anyone has posted a question on this specifically.)
looking at the log is pretty painful as any System.debug() lines appear to be dropped due to the size of the log being >18MB but it appears there are 40000 rows to be added. Having caved and looked at some of the other code dumps out there i dont see that anyone else looks to have had this issue or made any special arrangements with batching and as far as i can see i haven't done anything drastically different to them (but the subtlety is clearly more than enough to cause an issue).
So if anyone could have a look over this and give me a hint as to where/why i have gone wrong it would be a great help. I'm not looking for a link to a working code set to copy i'm hoping for some understanding as to where my approach is incorrect/causing issues
With my trigger
trigger MaintenanceRequest on Case ( after update) {
List<Case> closedCases = new List<Case>();
for (Case c : Trigger.New){
System.debug('Type :' + c.Type );
System.debug('Equals Repair: ' +c.Type.equalsIgnoreCase('Repair'));
System.debug('Equals Routine Maintenance: ' +c.Type.equalsIgnoreCase('Routine Maintenance'));
if (
(c.Type.equalsIgnoreCase('Repair') || c.Type.equalsIgnoreCase('Routine Maintenance'))
&& c.isClosed == true )
{
closedCases.add(c);
}
}
System.debug(closedCases.size() + ' cases were closed');
if(closedCases.size() > 0){
MaintenanceRequestHelper.createScheduledMaintainance(closedCases);
}
}
and helper
public with sharing class MaintenanceRequestHelper {
public MaintenanceRequestHelper() {
}
public static Map<Id,Integer> getMainainanceCycleForCases(){
List<AggregateResult> maintainTimes = [SELECT Maintenance_Request__c, MIN(Equipment__r.Maintenance_Cycle__c)cycle
FROM Equipment_Maintenance_Item__c
GROUP BY Maintenance_Request__c
];
System.debug(maintainTimes);
Map<Id,Integer> maintinTimeMap = new Map<Id,Integer>();
for( AggregateResult e : maintainTimes ){
maintinTimeMap.put((Id)e.get('Maintenance_Request__c'), ((Double)e.get('cycle')).intValue());
}
System.debug(maintinTimeMap);
return maintinTimeMap;
}
public static Map<Id,List<Id>> getMaintainanceItems(List<Case> closedCases){
// map maintainance_Request_ID, Equipment_Maintenance_Item__c
List<Equipment_Maintenance_Item__c> equipmentMaintainanceList = [SELECT Maintenance_Request__c,
Equipment__r.Id
FROM Equipment_Maintenance_Item__c
Where Maintenance_Request__c IN :closedCases];
Map<Id,List<Id>> maintainance_equipmentMap = new Map<Id, List<Id>>();
for (Equipment_Maintenance_Item__c e:equipmentMaintainanceList){
System.debug('Putting : '+ e.Maintenance_Request__c + ' : ' + e.Equipment__r.Id);
if(maintainance_equipmentMap.containsKey(e.Maintenance_Request__c)){
maintainance_equipmentMap.get(e.Maintenance_Request__c).add(e.Equipment__r.Id);
}else{
maintainance_equipmentMap.put(e.Maintenance_Request__c,new List<id>{e.Equipment__r.Id});
}
System.debug('Map now : '+ maintainance_equipmentMap);
}
System.debug('Returning: ' +maintainance_equipmentMap);
return maintainance_equipmentMap;
}
public static void createScheduledMaintainance(List<Case> closedCaseList) {
System.debug(closedCaseList.size() + ' Cases to create');
Map<Id,Integer> maintainTimeMap = getMainainanceCycleForCases();
Map<Id,List<Id>> maintainanceItems = getMaintainanceItems(closedCaseList); // map maintainance_Request_ID, Equipment_Maintenance_Item__c
List<Case> createNewCases = new List<Case>();
for(Case c : closedCaseList){
Case newCase = new Case();
newCase.Type = 'Routine Maintenance';
newCase.Status = 'New';
newCase.Vehicle__c = c.Vehicle__c;
newCase.Subject = c.Subject;
newCase.Date_Reported__c = Date.today();
newCase.Date_Due__c = Date.today() + maintainTimeMap.get(c.Id);
newCase.ProductId = c.ProductId;
createNewCases.add(newCase);
}
System.debug(createNewCases.size() +' to insert');
list<Equipment_Maintenance_Item__c> itemsListToinsert= new list<Equipment_Maintenance_Item__c>();
if(createNewCases.size()>0){
insert createNewCases;
for(Case ca : createNewCases){
for(Id key: maintainanceItems.keySet()){
List<Id> equipment = maintainanceItems.get(key);
for (Id e : equipment){
Equipment_Maintenance_Item__c newitem = new Equipment_Maintenance_Item__c();
newitem.Equipment__c=e;
newitem.Maintenance_Request__c= ca.Id;
itemsListToinsert.add(newitem);
}
}
}
}
System.debug('itemsListToinsert Size: ' +itemsListToinsert.size());
if (itemsListToinsert.size() >0){
insert itemsListToinsert; //<<<< ERROR TRIGGERS HERE and has ~40000 rows <<<<<
}
}
}
So, It seems i was over complicating the issue to an extent.
Having taken a break and walking through the problem again something just didnt sit right with this block of code.
if(createNewCases.size()>0){
insert createNewCases;
for(Case ca : createNewCases){
for(Id key: maintainanceItems.keySet()){
List<Id> equipment = maintainanceItems.get(key);
for (Id e : equipment){
Equipment_Maintenance_Item__c newitem = new Equipment_Maintenance_Item__c();
newitem.Equipment__c=e;
newitem.Maintenance_Request__c= ca.Id;
itemsListToinsert.add(newitem);
}
}
}
}
For -> For -> For -> create new Equipment_Maintenance_Item__c
With 0 checks to see if this new Equipment_Maintenance_Item__c was needed.
I ended up partly re-writing the class but the main change was re-working the section of code above to
List<Equipment_Maintenance_Item__c> equipmentMaintainanceList = [SELECT Maintenance_Request__c,
Equipment__r.Id
FROM Equipment_Maintenance_Item__c
Where Maintenance_Request__c IN :closedCaseList];
List<Equipment_Maintenance_Item__c> updatedEquipment = new List<Equipment_Maintenance_Item__c>();
for(Case closed : closedCaseList){
for(Equipment_Maintenance_Item__c eqip : equipmentMaintainanceList){
if (eqip.Maintenance_Request__c == closed.Id){
Equipment_Maintenance_Item__c e = eqip.clone(false, false, false, false);
e.Maintenance_Request__c = oldToNewMap.get(closed.Id).Id;
updatedEquipment.add(e);
}
}
}
if (updatedEquipment.size() > 0 ){
insert updatedEquipment;
}
For me the lesson is be very mindful of nested for loops when the outcome is going to come up against some hard and fast governor limits.
I have question on my Salesforce WebService and Apex Code.
In a relationship, we have one Notice and multiple attachments in my salesforce. But I don't know how to fix below requirements:
when "GET" webservices incoming thru specific URL API, it supposed to return with JSON format
JSON Format should {Notice1 : attach1{link},attach2{link} , etc }
#RestResource(urlMapping='/API/V1/notice/*')
global with sharing class API_Notice {
#HttpGet(UrlMapping='/API/V1/notice/all')
global static List<String> getNotice(){
Set<Id> NoticeIds = new Set<Id>();
Set<Id> VersionIds = new Set<Id>();
String compares;
List<String> returnJSON = new List<String>();
List<Notice__c> reConts = [select Id, ClosingDate__c ,Name, Contents__c from notice__c];
Map<Id,Notice__c> addMap = new Map <Id,Notice__c>();
Map<Map<Id,Notice__c>,Map<Id,contentdistribution>> addsMap = new Map<Map<Id,Notice__c>,Map<Id,contentdistribution>>();
//SET NOTICE ID
if(!reConts.isEmpty()){
for(Notice__c nc : reConts){
NoticeIds.add(nc.Id);
addMap.put(nc.id,nc);
}
}
//GET public Image URL
if(!NoticeIds.isEmpty()){
Map<Id,ContentDocumentLink> contentMap = new Map<Id,ContentDocumentLink>([
select contentDocumentid,LinkedEntityId from ContentDocumentLink where LinkedEntityId IN:NoticeIds
]);
for(ContentDocumentLink Key : contentMap.values()){
VersionIds.add(Key.ContentDocumentId);
}
if(!VersionIds.isEmpty()){
Map<Id, contentdistribution> cdb = new Map <Id, contentdistribution> ([
select DistributionPublicUrl from contentdistribution where contentDocumentid IN:VersionIds
]);
addsMap.put(addMap,cdb);
}
}
return null;
}
}
I wanted to deploy my code to production. In this apex code, I am calling a third party api for opportunity on click of button which triggers the doSomething() from VF page. I want to fix this issue and push the below code to my production account.
Here is my apex class code
{
private ApexPages.StandardController standardController;
public DetailButtonController(ApexPages.StandardController standardController)
{
this.standardController = standardController;
}
public PageReference doSomething()
{
// Apex code for handling record from a Detail page goes here
Id recordId = standardController.getId();
Opportunity record = (Opportunity) standardController.getRecord();
HttpRequest req = new HttpRequest();
HttpResponse res = new HttpResponse();
Http http = new Http();
req.setEndpoint('https://mergeasy.com/merge_file');
req.setMethod('POST');
//function to Convert date to mm/dd/yyy
Date dToday = record.Closing_Date__c;
String clos_date = 'On or before ' + dToday.month() + '/' + dToday.day() + '/' + dToday.year();
Date dAcc = record.Offer_Acceptance_Date__c;
String acc_date = dAcc.month() + '/' + dAcc.day() + '/' + dAcc.year();
String str1 = '' + record.Purchase_Price__c ;
String f_p_price = str1.SubStringBefore('.');
String str2 = '' + record.Escrow_Deposit__c ;
String e_d_price = str2.SubStringBefore('.');
String str3 = '' + record.Balance__c ;
String b_price = str3.SubStringBefore('.');
if(record.Second_Seller_Name_Phone__c==null && record.Second_Seller_Email__c==null && record.Name!=null && record.Company_Profile__c!=null){
req.setBody('seller_name='+record.Name+'&buyer_name='+record.Company_Profile__c+'&county='+record.County_Contract__c+'&street_address='+record.Left_Main__Address_1__c+'&p_price='+f_p_price+'&escrow_deposit='+e_d_price+'&title_agent='+record.Escrow_Agent_Name__c+'&title_address='+record.Escrow_Address__c+'&title_phone='+record.Escrow_Number__c+'&balance='+b_price+'&accept_date='+acc_date+'&closing_date='+clos_date+'&inspection_days='+record.Inspection_Days__c+'&special_clause='+record.Special_Clauses__c+'&doc_id=XXXXXXXXXX&doc_name=Contract.pdf&delivery_method=docusign&sign_order=true&recipient1_email='+record.Email__c+'&recipient1_name='+record.Name+'&recipient2_name='+record.Company_Profile__c+'&recipient2_email=developer.c2c#gmail.com&docusign_doc_name=Contract - Attorney Involved&email_subject=Contract:'+record.Left_Main__Address_1__c+'&email_body=Hi please sign the attached contract');
}
else if(record.Second_Seller_Name_Phone__c!=null && record.Second_Seller_Email__c!=null && record.Name!=null && record.Company_Profile__c!=null){
String name = record.Name + ' and ' + record.Second_Seller_Name_Phone__c ;
req.setBody('seller_name='+name+'&buyer_ame='+record.Company_Profile__c+'&county='+record.County_Contract__c+'&street_address='+record.Left_Main__Address_1__c+'&p_price='+f_p_price+'&escrow_deposit='+e_d_price+'&title_agent='+record.Escrow_Agent_Name__c+'&title_address='+record.Escrow_Address__c+'&title_phone='+record.Escrow_Number__c+'&balance='+b_price+'&accept_date='+acc_date+'&closing_date='+clos_date+'&inspection_days='+record.Inspection_Days__c+'&special_clause='+record.Special_Clauses__c+'&doc_id=XXXXXXXXXX&doc_name=Contract.pdf&delivery_method=docusign&sign_order=true&recipient1_email='+record.Email__c+'&recipient1_name='+record.Name+'&recipient2_name='+record.Second_Seller_Name_Phone__c+'&recipient2_email='+record.Second_Seller_Email__c+'&recipient3_email=developer.c2c#gmail.com&recipient3_name='+record.Company_Profile__c+'&docusign_doc_name=Contract - Normal(1S1B).pdf&email_subject=Contract:'+record.Left_Main__Address_1__c+'&email_body=Hi please sign the attached contract');
}
req.setHeader('Authorization', 'Bearer XXXXXXXXXXXXXX');
try {
res = http.send(req);
} catch(System.CalloutException e) {
System.debug('Callout error: '+ e);
System.debug(res.toString());
}
return null;
}
}
Here is the test class, which is showing 90% code coverage.
//testClasst.apxc
#isTest
public class testClassBt {
#isTest
static void testPostCallout() {
System.Test.setMock(HttpCalloutMock.class, new TestClass());
Opportunity opp = new Opportunity();
opp.Name='Rickson Developer';
opp.StageName='Underwrite';
opp.CloseDate= date.newInstance(1991, 2, 21);
opp.Closing_Date__c= date.newInstance(1991, 2, 21);
opp.Offer_Acceptance_Date__c =date.newInstance(1991, 2, 21);
opp.Purchase_Price__c = 1200.00;
opp.Escrow_Deposit__c= 1200.00;
opp.Company_Profile__c='RFTA Properties, LLC';
opp.County_Contract__c='Orange';
opp.Left_Main__Address_1__c='123 Main Street';
opp.Escrow_Agent_Name__c='Test Agent';
opp.Escrow_Address__c='123 Main street';
opp.Escrow_Number__c='9892132382';
opp.Inspection_Days__c=34;
opp.Special_Clauses__c='Test';
insert opp;
ApexPages.StandardController standardController = new ApexPages.StandardController(opp);
DetailButtonController strResp = new DetailButtonController(standardController);
strResp.doSomething();
}
}
//TestClass.apxc
#isTest
global class TestClass implements HttpCalloutMock {
global HTTPResponse respond(HTTPRequest request) {
HttpResponse response = new HttpResponse();
response.setHeader('Content-Type', 'application/json');
response.setBody('{"animal": {"id":1, "name":"Tiger"}}');
response.setStatusCode(200);
return response;
}
}
assuming that during the validation process you run just the test methods of this class, did you try to run your test class in Sandbox first?
Some IDE and the Salesforce Developer Console itself show you the covered lines after the unit test execution.
Just follow the green lines to debug the code and understand where the exception has been thrown.
If you could post the Test class too, we can help you more.
Using Net Core 3.1 with Microsoft Identity Web and Azure AD.
I'm trying to setup some logging for when a user signs in and out of my web app project. The logging needs to include details of the user as well as the IP Address of the client endpoint they used during sign in and sign out. I then pass the IP Address through an extension method for capturing Geo Location info that is added to the log event for that user authentication.
In startup.cs I have configured some extended options for the OpenIdConnectOptions, they are:
OnTokenValidated
OnRedirectToIdentityProviderForSignOut
OnSignedOutCallbackRedirect
The OpenIdEvents class I created is just simply to move away the methods from the startup.cs file for cleanliness.
Extract from startup.cs below:
// Create a new instance of the class that stores the methods called
// by OpenIdConnectEvents(); i.e. when a user logs in or out the app.
// See section below :- 'services.Configure'
OpenIdEvents openIdEvents = new OpenIdEvents();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// The claim in the Jwt token where App roles are available.
options.TokenValidationParameters.RoleClaimType = "roles";
// Advanced config - capturing user events. See OpenIdEvents class.
options.Events ??= new OpenIdConnectEvents();
options.Events.OnTokenValidated += openIdEvents.OnTokenValidatedFunc;
// This is event is fired when the user is redirected to the MS Signout Page (before they've physically signed out)
options.Events.OnRedirectToIdentityProviderForSignOut += openIdEvents.OnRedirectToIdentityProviderForSignOutFunc;
// DO NOT DELETE - May use in the future.
// OnSignedOutCallbackRedirect doesn't produce any claims to read for the user after they have signed out.
options.Events.OnSignedOutCallbackRedirect += openIdEvents.OnSignedOutCallbackRedirectFunc;
});
So far I have found a solution to capture the required claims of the user for when they sign in, the 'TokenValidatedContext' passed to the first method 'OnTokenValidatedFunc' contains details of the security token which in itself shows the optional claims that I had configured including the IP Address (referred to as "ipaddr")
Some of these optional claims were configured in the App manifest file in Azure, they are present in the security token in this first method so pretty sure Azure is setup correctly.
Extract from Azure App Manifest File:
"optionalClaims": {
"idToken": [
{
"name": "family_name",
"source": null,
"essential": false,
"additionalProperties": []
},
{
"name": "given_name",
"source": null,
"essential": false,
"additionalProperties": []
},
{
"name": "ipaddr",
"source": null,
"essential": false,
"additionalProperties": []
}
],
"accessToken": [],
"saml2Token": []
},
'OnTokenValidatedFunc' method shown below:
/// <summary>
/// Invoked when an IdToken has been validated and produced an AuthenticationTicket.
/// See weblink: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.ontokenvalidated?view=aspnetcore-3.0
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnTokenValidatedFunc(TokenValidatedContext context)
{
var token = context.SecurityToken;
var userId = token.Claims.First(claim => claim.Type == "oid").Value;
var givenName = token.Claims.First(claim => claim.Type == "given_name").Value;
var familyName = token.Claims.First(claim => claim.Type == "family_name").Value;
var userName = token.Claims.First(claim => claim.Type == "preferred_username").Value;
string ipAddress = token.Claims.First(claim => claim.Type == "ipaddr").Value;
GeoHelper geoHelper = new GeoHelper();
var geoInfo = await geoHelper.GetGeoInfo(ipAddress);
string logEventCategory = "Open Id Connect";
string logEventType = "User Login";
string logEventSource = "WebApp_RAZOR";
string logCountry = "";
string logRegionName = "";
string logCity = "";
string logZip = "";
string logLatitude = "";
string logLongitude = "";
string logIsp = "";
string logMobile = "";
string logUserId = userId;
string logUserName = userName;
string logForename = givenName;
string logSurname = familyName;
string logData = "User login";
if (geoInfo != null)
{
logCountry = geoInfo.Country;
logRegionName = geoInfo.RegionName;
logCity = geoInfo.City;
logZip = geoInfo.Zip;
logLatitude = geoInfo.Latitude.ToString();
logLongitude = geoInfo.Longitude.ToString();
logIsp = geoInfo.Isp;
logMobile = geoInfo.Mobile.ToString();
}
// Tested on 31/08/2020
Log.Information(
"{#LogEventCategory}" +
"{#LogEventType}" +
"{#LogEventSource}" +
"{#LogCountry}" +
"{#LogRegion}" +
"{#LogCity}" +
"{#LogZip}" +
"{#LogLatitude}" +
"{#LogLongitude}" +
"{#LogIsp}" +
"{#LogMobile}" +
"{#LogUserId}" +
"{#LogUsername}" +
"{#LogForename}" +
"{#LogSurname}" +
"{#LogData}",
logEventCategory,
logEventType,
logEventSource,
logCountry,
logRegionName,
logCity,
logZip,
logLatitude,
logLongitude,
logIsp,
logMobile,
logUserId,
logUserName,
logForename,
logSurname,
logData);
await Task.CompletedTask.ConfigureAwait(false);
}
See Debug shots below:
When expanding the claims, you can see the claim for "ipaddr" is shown:
MY ISSUE:
The other event types fired from OpenIdConnectEvents for when the user signs out, does not function in the same way and this is where I am stuck!
There are two different event types I have tried testing with:
OnRedirectToIdentityProviderForSignOut
OnSignedOutCallbackRedirect
Each one is fired at a slightly different point during the user sign out process i.e. the 'OnRedirectToIdentityProviderForSignOutFunc' is fired when the user is being re-directed to the Microsoft Sign Out page, just before they actually hit the button and sign out.
This is not an ideal event type to work with given the user could abort signing out of the application and the log generated would not reflect this, however I have so far found that I could at least access most of the claims of the user, BUT the "ipaddr" claim is not listed and I simply don't know why or how to get it.
When I look at the Debug info I find the security token is not shown at all and the only way to access the user claims was to read another part of the context by navigating to context.HttpContext.User.Claims
Debug Screenshot:
The method for this shown below:
public async Task OnRedirectToIdentityProviderForSignOutFunc(RedirectContext context)
{
var user = context.HttpContext.User;
string ipAddress = user.Claims.FirstOrDefault(claim => claim.Type == "ipaddr").Value;
var userId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var givenName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
var familyName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
var userName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
// The IP Address claim is missing!
//string ipAddress = claims.First(claim => claim.Type == "ipaddr").Value;
await Task.CompletedTask.ConfigureAwait(false);
}
The above method only gives a partial solution given I still need the IP Address claim but is not present at all, but the choice in using this event type as explained above is not ideal anyway.
AND FINALLY:
Trying to subscribe to the final option 'OnSignedOutCallbackRedirect' has been a complete waste of time so far given none of the user claims are present at all in the context. It seems that Microsoft dumps them once the user has hit the Sign out button and returned back to the 'Signed Out' page in the web app.
Really I want a solution for when the user has actually signed out, not half way through the process of signing out, BUT I must be able access the user claims including the IP Address which is not present in either of the above two events fired during this process.
All I want is to simply capture the details (claims) of the user and the IP Address of the client session they are connecting from and log this when they sign in and sign out of the web application. Is this really too much to ask!
Documentation on this is very sparse, I would much appreciate some clues from anyone out there who understands well how MS Identity Web and OpenIDConnect Events function behind the scenes.
Solution 1 = Being able to access the IP Address claim from the context during 'OnRedirectToIdentityProviderForSignOut' but it is currently missing...
Solution 2 (Preferred) = Being able to access the user claims during 'OnSignedOutCallbackRedirect' but currently none of them are listed at all.
Thanks in advance...
I need to be able to access the claims from the user once they have signed out of the application using one of two possible events that are generated from OpenIdConnect
The user signed out at that point. He/She is no longer there, so it makes sense it has no claims, it's going back to the default, empty anonymous user because it's signed out.
As mentioned in my comments above, and also taking on board Jean-Marc Prieur's comments above on the fact that no claims will ever be present once the user has completely signed out, I ended up just grabbing the details of the user context through OnRedirectToIdentityProviderForSignOutFunc method and then used a separate GeoHelper class to fetch the IP Address of the destination where the person was when singing out (or should we say was about to sign out!
Yes appreciate this isn't the most ideal cause & affect, but to be honest it's not going to present an big issue for me and may not for others given in most cases, logging when someone signs out is not business critical, its more to get a idea of system usage. By the time someone has reached the MS popup page to logout, we should likely assume 99% of cases that they will proceed and actually logout.
So below is the code I used to achieve the above scenario:
startup.cs class (extract from startup.cs code bloat)
// Create a new instance of the class that stores the methods called
// by OpenIdConnectEvents(); i.e. when a user logs in or out the app.
// See section below :- 'services.Configure'
OpenIdEvents openIdEvents = new OpenIdEvents();
// The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the Authorize attribute and User.IsInrole()
// See https://learn.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 for more info.
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// The claim in the Jwt token where App roles are available.
options.TokenValidationParameters.RoleClaimType = "roles";
// Advanced config - capturing user events. See OpenIdEvents class.
options.Events ??= new OpenIdConnectEvents();
options.Events.OnTokenValidated += openIdEvents.OnTokenValidatedFunc;
// This is event is fired when the user is redirected to the MS Signout Page (before they've physically signed out)
options.Events.OnRedirectToIdentityProviderForSignOut += openIdEvents.OnRedirectToIdentityProviderForSignOutFunc;
// DO NOT DELETE - May use in the future.
// OnSignedOutCallbackRedirect doesn't produce any user claims to read from for the user after they have signed out.
options.Events.OnSignedOutCallbackRedirect += openIdEvents.OnSignedOutCallbackRedirectFunc;
});
My custom class for the Geolocation:
namespace MyProject.Classes.GeoLocation
{
/// <summary>
/// See weblink for API documentation: https://ip-api.com/docs or https://ip-api.com/docs/api:json
/// Note: Not free for commercial use - fee plan during development only!
/// Sample query: http://ip-api.com/json/{ip_address}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,isp,mobile,query
/// </summary>
public class GeoHelper
{
private readonly HttpClient _httpClient;
public GeoHelper()
{
_httpClient = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(5)
};
}
public async Task<GeoInfo> GetGeoInfo(string ip)
{
try
{
var response = await _httpClient.GetAsync($"http://ip-api.com/json/{ip}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,isp,mobile,query");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<GeoInfo>(json);
}
}
catch (Exception)
{
// Do nothing, just return null.
}
return null;
}
}
}
OpenIdEvents.cs class:
namespace MyProject.Classes.Security
{
public class OpenIdEvents
{
// Create the concurrent dictionary to store the user's IP Addresss when they sign in, the value is fetched
// from the dictionary when they sing out. given this information is not present within the contect passed through the event.
private readonly ConcurrentDictionary<string, string> IpAddressDictionary = new ConcurrentDictionary<string, string>();
/// <summary>
/// Invoked when an IdToken has been validated and produced an AuthenticationTicket.
/// See weblink: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.ontokenvalidated?view=aspnetcore-3.0
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnTokenValidatedFunc(TokenValidatedContext context)
{
var token = context.SecurityToken;
var userId = token.Claims.First(claim => claim.Type == "oid").Value;
var givenName = token.Claims.First(claim => claim.Type == "given_name").Value;
var familyName = token.Claims.First(claim => claim.Type == "family_name").Value;
var username = token.Claims.First(claim => claim.Type == "preferred_username").Value;
var ipAddress = token.Claims.First(claim => claim.Type == "ipaddr").Value;
// Add the IP Address from the user's ID Token to the dictionary, we will remove
// it from the dictionary when the user requests a sign out through OpenIDConnect.
IpAddressDictionary.TryAdd(userId, ipAddress);
GeoHelper geoHelper = new GeoHelper();
var geoInfo = await geoHelper.GetGeoInfo(ipAddress);
string logEventCategory = "Open Id Connect";
string logEventType = "User Sign In";
string logEventSource = "MyProject";
string logCountry = "";
string logRegionName = "";
string logCity = "";
string logZip = "";
string logLatitude = "";
string logLongitude = "";
string logIsp = "";
string logMobile = "";
string logUserId = userId;
string logUserName = username;
string logForename = givenName;
string logSurname = familyName;
string logData = "User with username [" + username + "] forename [" + givenName + "] surname [" + familyName + "] from IP Address [" + ipAddress + "] signed into the application [MyProject] Succesfully";
if (geoInfo != null)
{
logCountry = geoInfo.Country;
logRegionName = geoInfo.RegionName;
logCity = geoInfo.City;
logZip = geoInfo.Zip;
logLatitude = geoInfo.Latitude.ToString();
logLongitude = geoInfo.Longitude.ToString();
logIsp = geoInfo.Isp;
logMobile = geoInfo.Mobile.ToString();
}
// Tested on 31/08/2020
Log.Information(
"{#LogEventCategory}" +
"{#LogEventType}" +
"{#LogEventSource}" +
"{#LogCountry}" +
"{#LogRegion}" +
"{#LogCity}" +
"{#LogZip}" +
"{#LogLatitude}" +
"{#LogLongitude}" +
"{#LogIsp}" +
"{#LogMobile}" +
"{#LogUserId}" +
"{#LogUsername}" +
"{#LogForename}" +
"{#LogSurname}" +
"{#LogData}",
logEventCategory,
logEventType,
logEventSource,
logCountry,
logRegionName,
logCity,
logZip,
logLatitude,
logLongitude,
logIsp,
logMobile,
logUserId,
logUserName,
logForename,
logSurname,
logData);
await Task.CompletedTask.ConfigureAwait(false);
}
/// <summary>
/// Invoked before redirecting to the identity provider to sign out.
/// See weblink: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onredirecttoidentityproviderforsignout?view=aspnetcore-3.0&viewFallbackFrom=aspnetcore-3.1
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnRedirectToIdentityProviderForSignOutFunc(RedirectContext context)
{
var user = context.HttpContext.User;
var userId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var givenName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
var familyName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
var username = user.Identity.Name;
string logEventCategory = "Open Id Connect";
string logEventType = "User Sign Out";
string logEventSource = "MyProject";
string logCountry = "";
string logRegionName = "";
string logCity = "";
string logZip = "";
string logLatitude = "";
string logLongitude = "";
string logIsp = "";
string logMobile = "";
string logUserId = userId;
string logUserName = username;
string logForename = givenName;
string logSurname = familyName;
IpAddressDictionary.TryRemove(userId, out string ipAddress);
if (ipAddress != null)
{
// Re-fetch the geo-location details which may be different than the login session
// given the user might have been signed in using a cell phone and move locations.
GeoHelper geoHelper = new GeoHelper();
var geoInfo = await geoHelper.GetGeoInfo(ipAddress);
if (geoInfo != null)
{
logCountry = geoInfo.Country;
logRegionName = geoInfo.RegionName;
logCity = geoInfo.City;
logZip = geoInfo.Zip;
logLatitude = geoInfo.Latitude.ToString();
logLongitude = geoInfo.Longitude.ToString();
logIsp = geoInfo.Isp;
logMobile = geoInfo.Mobile.ToString();
}
}
string logData = "User with username [" + username + "] forename [" + givenName + "] surname [" + familyName + "] from IP Address [" + ipAddress + "] signed out the application [MyProject] Succesfully";
// Tested on 31/08/2020
Log.Information(
"{#LogEventCategory}" +
"{#LogEventType}" +
"{#LogEventSource}" +
"{#LogCountry}" +
"{#LogRegion}" +
"{#LogCity}" +
"{#LogZip}" +
"{#LogLatitude}" +
"{#LogLongitude}" +
"{#LogIsp}" +
"{#LogMobile}" +
"{#LogUserId}" +
"{#LogUsername}" +
"{#LogForename}" +
"{#LogSurname}" +
"{#LogData}",
logEventCategory,
logEventType,
logEventSource,
logCountry,
logRegionName,
logCity,
logZip,
logLatitude,
logLongitude,
logIsp,
logMobile,
logUserId,
logUserName,
logForename,
logSurname,
logData);
await Task.CompletedTask.ConfigureAwait(false);
}
/// <summary>
/// Invoked before redirecting to the SignedOutRedirectUri at the end of a remote sign-out flow.
/// See weblink: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onsignedoutcallbackredirect?view=aspnetcore-3.0
/// Not currently in use becuase neither the user's ID Token or claims were present. We had to use the above method instead.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnSignedOutCallbackRedirectFunc(RemoteSignOutContext context)
{
await Task.CompletedTask.ConfigureAwait(false);
}
}
}
I am trying to send attachment (record has one attachment) in opportunity record via Apex and Docusign "CreateAndSendEnvelope" API.
But I am getting this error "The DocuSign EnvelopeId:Exception - System.CalloutException: Web service callout failed: WebService returned a SOAP Fault: An Error Occurred during anchor tag processing. Invalid document faultcode=soap:Client faultactor=https://demo.docusign.net/api/3.0/dsapi.asmx"
Below is the piece of code used.
// Render the contract
System.debug('Rendering the contract');
PageReference pageRef = new PageReference('/apex/RenderContract');
pageRef.getParameters().put('id',contract.Id);
//Blob pdfBlob = pageRef.getContent();
Attachment att = [SELECT Id, Name, Body, ContentType FROM Attachment WHERE Parentid = :contract.Id LIMIT 1];
Blob pdfBlob = att.Body;
// Document
DocuSignAPI.Document document = new DocuSignAPI.Document();
document.ID = 1;
document.pdfBytes = EncodingUtil.base64Encode(pdfBlob);
document.Name = 'Contract';
document.FileExtension = 'pdf';
envelope.Documents = new DocuSignAPI.ArrayOfDocument();
envelope.Documents.Document = new DocuSignAPI.Document[1];
envelope.Documents.Document[0] = document;
// Recipient
System.debug('getting the contact');
Contact contact = [SELECT email, FirstName, LastName
from Contact where id = :contract.CustomerSignedId];
DocuSignAPI.Recipient recipient = new DocuSignAPI.Recipient();
recipient.ID = 1;
recipient.Type_x = 'Signer';
recipient.RoutingOrder = 1;
recipient.Email = contact.Email;
recipient.UserName = contact.FirstName + ' ' + contact.LastName;
// This setting seems required or you see the error:
// "The string '' is not a valid Boolean value.
// at System.Xml.XmlConvert.ToBoolean(String s)"
recipient.RequireIDLookup = false;
envelope.Recipients = new DocuSignAPI.ArrayOfRecipient();
envelope.Recipients.Recipient = new DocuSignAPI.Recipient[1];
envelope.Recipients.Recipient[0] = recipient;
// Tab
DocuSignAPI.Tab tab1 = new DocuSignAPI.Tab();
tab1.Type_x = 'SignHere';
tab1.RecipientID = 1;
tab1.DocumentID = 1;
tab1.AnchorTabItem = new DocuSignAPI.AnchorTab();
tab1.AnchorTabItem.AnchorTabString = 'By:';
DocuSignAPI.Tab tab2 = new DocuSignAPI.Tab();
tab2.Type_x = 'DateSigned';
tab2.RecipientID = 1;
tab2.DocumentID = 1;
tab2.AnchorTabItem = new DocuSignAPI.AnchorTab();
tab2.AnchorTabItem.AnchorTabString = 'Date Signed:';
envelope.Tabs = new DocuSignAPI.ArrayOfTab();
envelope.Tabs.Tab = new DocuSignAPI.Tab[2];
envelope.Tabs.Tab[0] = tab1;
envelope.Tabs.Tab[1] = tab2;
System.debug('Calling the API');
try {
DocuSignAPI.EnvelopeStatus es
= dsApiSend.CreateAndSendEnvelope(envelope);
envelopeId = es.EnvelopeID;
} catch ( CalloutException e) {
System.debug('Exception - ' + e );
envelopeId = 'Exception - ' + e;
}
Any ideas how to overcome this error?
Thanks.
The Original Poster's (OP's) comment is
it worked fine on rendering the whole record to pdf...but now i tried sending attachments only instead of whole record.. i started to get this error.
So my guess is that the envelope request has a document problem.
Best way to debug: see what is being sent to the DocuSign platform.
Try the beta API logger or the regular logger. Then add the log to your question by editing your question.
This problem came across me with same error .
" An Error Occurred during anchor tag processing. Invalid document faultcode=soap:Client faultactor=https://demo.docusign.net/api/3.0/dsapi.asmx "
you need to replace anchor tab string with desired string given in your attached document where signature is required.
Replace :
tab1.AnchorTabItem.AnchorTabString = 'By:';
tab2.AnchorTabItem.AnchorTabString = 'Date Signed:';
To :
tab1.AnchorTabItem.AnchorTabString = 'Signature label in your document';
tab2.AnchorTabItem.AnchorTabString = 'Signature label in your document';