mirror of
https://github.com/danieleteti/delphimvcframework.git
synced 2024-11-15 07:45:54 +01:00
2.1.6 (carbon)
FIX https://github.com/danieleteti/delphimvcframework/issues/74 Updated jsonwebtoken sample Improved support for customclaims into the controller actions
This commit is contained in:
parent
3630dbe076
commit
7aa5dd1ccb
@ -1,3 +1,8 @@
|
||||
2.1.6 (carbon)
|
||||
FIX https://github.com/danieleteti/delphimvcframework/issues/74
|
||||
Updated jsonwebtoken sample
|
||||
Improved support for customclaims into the controller actions
|
||||
|
||||
2.1.4 (beryllium)
|
||||
|
||||
FIX https://github.com/danieleteti/delphimvcframework/issues/71
|
||||
|
@ -41,7 +41,7 @@ type
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SysUtils, System.JSON, System.Classes;
|
||||
System.SysUtils, System.JSON, System.Classes, System.Generics.Collections;
|
||||
|
||||
{ TApp1MainController }
|
||||
|
||||
@ -58,12 +58,20 @@ end;
|
||||
{ TAdminController }
|
||||
|
||||
procedure TAdminController.OnlyRole1(ctx: TWebContext);
|
||||
var
|
||||
lPair: TPair<String, String>;
|
||||
begin
|
||||
ContentType := TMVCMediaType.TEXT_PLAIN;
|
||||
ResponseStream.AppendLine('Hey! Hello ' + ctx.LoggedUser.UserName +
|
||||
', now you are a logged user and this is a protected content!');
|
||||
ResponseStream.AppendLine('As logged user you have the following roles: ' +
|
||||
sLineBreak + string.Join(sLineBreak, Context.LoggedUser.Roles.ToArray));
|
||||
ResponseStream.AppendLine('You CustomClaims are: ' +
|
||||
sLineBreak);
|
||||
for lPair in Context.LoggedUser.CustomData do
|
||||
begin
|
||||
ResponseStream.AppendFormat('%s = %s' + sLineBreak, [lPair.Key, lPair.Value]);
|
||||
end;
|
||||
RenderResponseStream;
|
||||
end;
|
||||
|
||||
@ -73,6 +81,7 @@ var
|
||||
lJArr: TJSONArray;
|
||||
lQueryParams: TStrings;
|
||||
I: Integer;
|
||||
lPair: TPair<String, String>;
|
||||
begin
|
||||
ContentType := TMVCMediaType.APPLICATION_JSON;
|
||||
lJObj := TJSONObject.Create;
|
||||
@ -88,6 +97,13 @@ begin
|
||||
lQueryParams.ValueFromIndex[I])));
|
||||
end;
|
||||
|
||||
lJArr := TJSONArray.Create;
|
||||
lJObj.AddPair('customclaims', lJArr);
|
||||
for lPair in Context.LoggedUser.CustomData do
|
||||
begin
|
||||
lJArr.AddElement(TJSONObject.Create(TJSONPair.Create(lPair.Key, lPair.Value)));
|
||||
end;
|
||||
|
||||
Render(lJObj);
|
||||
end;
|
||||
|
||||
|
@ -11,9 +11,9 @@ type
|
||||
protected
|
||||
procedure OnRequest(const ControllerQualifiedClassName: string;
|
||||
const ActionName: string; var AuthenticationRequired: Boolean);
|
||||
procedure OnAuthentication(const UserName: string; const Password: string;
|
||||
UserRoles: System.Generics.Collections.TList<System.string>;
|
||||
var IsValid: Boolean; const SessionData: TSessionData);
|
||||
procedure OnAuthentication(const UserName,
|
||||
Password: string; UserRoles: System.Generics.Collections.TList<System.string>;
|
||||
var IsValid: Boolean; const CustomData: TMVCCustomData);
|
||||
procedure OnAuthorization(UserRoles
|
||||
: System.Generics.Collections.TList<System.string>;
|
||||
const ControllerQualifiedClassName: string; const ActionName: string;
|
||||
@ -26,7 +26,7 @@ implementation
|
||||
|
||||
procedure TAuthenticationSample.OnAuthentication(const UserName,
|
||||
Password: string; UserRoles: System.Generics.Collections.TList<System.string>;
|
||||
var IsValid: Boolean; const SessionData: TSessionData);
|
||||
var IsValid: Boolean; const CustomData: TMVCCustomData);
|
||||
begin
|
||||
IsValid := UserName.Equals(Password); // hey!, this is just a demo!!!
|
||||
if IsValid then
|
||||
@ -44,6 +44,8 @@ begin
|
||||
UserRoles.Add('role1');
|
||||
UserRoles.Add('role2');
|
||||
end;
|
||||
CustomData.Add('customclaim1', 'hello world');
|
||||
CustomData.Add('customclaim2', 'daniele teti');
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
@ -2,8 +2,8 @@ object Form5: TForm5
|
||||
Left = 0
|
||||
Top = 0
|
||||
Caption = 'Form5'
|
||||
ClientHeight = 379
|
||||
ClientWidth = 513
|
||||
ClientHeight = 460
|
||||
ClientWidth = 647
|
||||
Color = clBtnFace
|
||||
Font.Charset = DEFAULT_CHARSET
|
||||
Font.Color = clWindowText
|
||||
@ -14,18 +14,31 @@ object Form5: TForm5
|
||||
PixelsPerInch = 96
|
||||
TextHeight = 13
|
||||
object Splitter1: TSplitter
|
||||
Left = 0
|
||||
Top = 309
|
||||
Width = 647
|
||||
Height = 3
|
||||
Cursor = crVSplit
|
||||
Align = alBottom
|
||||
ExplicitLeft = -16
|
||||
ExplicitTop = 302
|
||||
ExplicitWidth = 513
|
||||
end
|
||||
object Splitter2: TSplitter
|
||||
Left = 0
|
||||
Top = 147
|
||||
Width = 513
|
||||
Width = 647
|
||||
Height = 3
|
||||
Cursor = crVSplit
|
||||
Align = alTop
|
||||
ExplicitWidth = 30
|
||||
ExplicitLeft = -8
|
||||
ExplicitTop = 302
|
||||
ExplicitWidth = 513
|
||||
end
|
||||
object Memo1: TMemo
|
||||
Left = 0
|
||||
Top = 49
|
||||
Width = 513
|
||||
Width = 647
|
||||
Height = 98
|
||||
Align = alTop
|
||||
Font.Charset = ANSI_CHARSET
|
||||
@ -36,12 +49,13 @@ object Form5: TForm5
|
||||
ParentFont = False
|
||||
ReadOnly = True
|
||||
TabOrder = 0
|
||||
ExplicitWidth = 513
|
||||
end
|
||||
object Memo2: TMemo
|
||||
Left = 0
|
||||
Top = 150
|
||||
Width = 513
|
||||
Height = 229
|
||||
Width = 647
|
||||
Height = 159
|
||||
Align = alClient
|
||||
Font.Charset = ANSI_CHARSET
|
||||
Font.Color = clWindowText
|
||||
@ -51,14 +65,17 @@ object Form5: TForm5
|
||||
ParentFont = False
|
||||
ReadOnly = True
|
||||
TabOrder = 1
|
||||
ExplicitWidth = 513
|
||||
ExplicitHeight = 229
|
||||
end
|
||||
object Panel1: TPanel
|
||||
Left = 0
|
||||
Top = 0
|
||||
Width = 513
|
||||
Width = 647
|
||||
Height = 49
|
||||
Align = alTop
|
||||
TabOrder = 2
|
||||
ExplicitWidth = 513
|
||||
object btnGet: TButton
|
||||
AlignWithMargins = True
|
||||
Left = 171
|
||||
@ -82,4 +99,19 @@ object Form5: TForm5
|
||||
OnClick = btnLOGINClick
|
||||
end
|
||||
end
|
||||
object Memo3: TMemo
|
||||
Left = 0
|
||||
Top = 312
|
||||
Width = 647
|
||||
Height = 148
|
||||
Align = alBottom
|
||||
Font.Charset = ANSI_CHARSET
|
||||
Font.Color = clWindowText
|
||||
Font.Height = -13
|
||||
Font.Name = 'Courier New'
|
||||
Font.Style = []
|
||||
ParentFont = False
|
||||
ReadOnly = True
|
||||
TabOrder = 3
|
||||
end
|
||||
end
|
||||
|
@ -15,6 +15,8 @@ type
|
||||
btnGet: TButton;
|
||||
btnLOGIN: TButton;
|
||||
Splitter1: TSplitter;
|
||||
Memo3: TMemo;
|
||||
Splitter2: TSplitter;
|
||||
procedure btnGetClick(Sender: TObject);
|
||||
procedure btnLOGINClick(Sender: TObject);
|
||||
private
|
||||
@ -42,6 +44,7 @@ var
|
||||
lResp: IRESTResponse;
|
||||
lQueryStringParams: TStringList;
|
||||
begin
|
||||
{ Getting JSON response }
|
||||
lClient := TRESTClient.Create('localhost', 8080);
|
||||
try
|
||||
lClient.ReadTimeOut(0);
|
||||
@ -52,10 +55,8 @@ begin
|
||||
lQueryStringParams.Values['firstname'] := 'Daniele';
|
||||
lQueryStringParams.Values['lastname'] := 'Teti';
|
||||
lResp := lClient.doGET('/admin/role1', [], lQueryStringParams);
|
||||
|
||||
if lResp.HasError then
|
||||
ShowMessage(lResp.Error.ExceptionMessage);
|
||||
|
||||
finally
|
||||
lQueryStringParams.Free;
|
||||
end;
|
||||
@ -63,6 +64,28 @@ begin
|
||||
finally
|
||||
lClient.Free;
|
||||
end;
|
||||
|
||||
{ Getting HTML response }
|
||||
lClient := TRESTClient.Create('localhost', 8080);
|
||||
try
|
||||
lClient.ReadTimeOut(0);
|
||||
if not FJWT.IsEmpty then
|
||||
lClient.RequestHeaders.Values['Authentication'] := 'bearer ' + FJWT;
|
||||
lQueryStringParams := TStringList.Create;
|
||||
try
|
||||
lQueryStringParams.Values['firstname'] := 'Daniele';
|
||||
lQueryStringParams.Values['lastname'] := 'Teti';
|
||||
lResp := lClient.Accept('text/html').doGET('/admin/role1', [], lQueryStringParams);
|
||||
if lResp.HasError then
|
||||
ShowMessage(lResp.Error.ExceptionMessage);
|
||||
finally
|
||||
lQueryStringParams.Free;
|
||||
end;
|
||||
Memo3.Lines.Text := lResp.BodyAsString;
|
||||
finally
|
||||
lClient.Free;
|
||||
end;
|
||||
|
||||
end;
|
||||
|
||||
procedure TForm5.btnLOGINClick(Sender: TObject);
|
||||
|
@ -36,7 +36,7 @@ uses
|
||||
{$ELSE}
|
||||
, Data.DBXJSON
|
||||
{$ENDIF}
|
||||
, MVCFramework.Patches
|
||||
, MVCFramework.Patches, MVCFramework
|
||||
;
|
||||
|
||||
type
|
||||
@ -182,6 +182,7 @@ type
|
||||
|
||||
TJWTCustomClaims = class(TJWTDictionaryObject)
|
||||
property Items; default;
|
||||
function AsCustomData: TMVCCustomData;
|
||||
end;
|
||||
|
||||
TJWT = class
|
||||
@ -341,6 +342,13 @@ begin
|
||||
Items[Index] := IntToStr(DateTimeToUnix(Value, False));
|
||||
end;
|
||||
|
||||
{ TJWTCustomClaims }
|
||||
|
||||
function TJWTCustomClaims.AsCustomData: TMVCCustomData;
|
||||
begin
|
||||
Result := TMVCCustomData.Create(FClaims);
|
||||
end;
|
||||
|
||||
{ TJWT }
|
||||
|
||||
function TJWT.CheckExpirationTime(Payload: TJSONObject;
|
||||
|
@ -48,7 +48,7 @@ type
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.StrUtils, MVCFramework.Commons;
|
||||
System.StrUtils, MVCFramework.Commons, System.Classes;
|
||||
|
||||
{ TCORSMiddleware }
|
||||
|
||||
@ -74,17 +74,14 @@ end;
|
||||
|
||||
procedure TCORSMiddleware.OnBeforeRouting(Context: TWebContext;
|
||||
var Handled: Boolean);
|
||||
var
|
||||
lCustomHeaders: TStrings;
|
||||
begin
|
||||
Context.Response.RawWebResponse.CustomHeaders.Values
|
||||
['Access-Control-Allow-Origin'] := FAllowedOriginURL;
|
||||
Context.Response.RawWebResponse.CustomHeaders.Values
|
||||
['Access-Control-Allow-Methods'] :=
|
||||
'POST, GET, OPTIONS, PUT, DELETE';
|
||||
Context.Response.RawWebResponse.CustomHeaders.Values
|
||||
['Access-Control-Allow-Headers'] := 'Content-Type, Accept';
|
||||
Context.Response.RawWebResponse.CustomHeaders.Values
|
||||
['Access-Control-Allow-Credentials'] := FAllowsCredentials;
|
||||
|
||||
lCustomHeaders := Context.Response.RawWebResponse.CustomHeaders;
|
||||
lCustomHeaders.Values['Access-Control-Allow-Origin'] := FAllowedOriginURL;
|
||||
lCustomHeaders.Values['Access-Control-Allow-Methods'] := 'POST, GET, OPTIONS, PUT, DELETE';
|
||||
lCustomHeaders.Values['Access-Control-Allow-Headers'] := 'Content-Type, Accept, jwtusername, jwtpassword, authentication';
|
||||
lCustomHeaders.Values['Access-Control-Allow-Credentials'] := FAllowsCredentials;
|
||||
if Context.Request.HTTPMethod = httpOPTIONS then
|
||||
begin
|
||||
Context.Response.StatusCode := 200;
|
||||
|
@ -49,6 +49,7 @@ type
|
||||
private
|
||||
FClaimsToChecks: TJWTCheckableClaims;
|
||||
FSetupJWTClaims: TJWTClaimsSetup;
|
||||
FLoginURLSegment: string;
|
||||
|
||||
protected
|
||||
FSecret: string;
|
||||
@ -62,6 +63,7 @@ type
|
||||
constructor Create(AMVCAuthenticationHandler: IMVCAuthenticationHandler;
|
||||
aConfigClaims: TJWTClaimsSetup;
|
||||
aSecret: string = 'D3lph1MVCFram3w0rk';
|
||||
aLoginURLSegment: string = '/login';
|
||||
aClaimsToCheck: TJWTCheckableClaims = [
|
||||
TJWTCheckableClaim.ExpirationTime,
|
||||
TJWTCheckableClaim.NotBefore,
|
||||
@ -73,31 +75,47 @@ implementation
|
||||
|
||||
uses
|
||||
MVCFramework.Session
|
||||
{$IFDEF SYSTEMJSON}
|
||||
|
||||
{$IFDEF SYSTEMJSON}
|
||||
|
||||
, System.JSON
|
||||
{$ELSE}
|
||||
|
||||
{$ELSE}
|
||||
|
||||
, Data.DBXJSON
|
||||
{$ENDIF}
|
||||
{$IFDEF WEBAPACHEHTTP}
|
||||
|
||||
{$ENDIF}
|
||||
{$IFDEF WEBAPACHEHTTP}
|
||||
|
||||
, Web.ApacheHTTP
|
||||
{$ENDIF}
|
||||
{$IFDEF SYSTEMNETENCODING}
|
||||
|
||||
{$ENDIF}
|
||||
{$IFDEF SYSTEMNETENCODING}
|
||||
|
||||
, System.NetEncoding
|
||||
{$ELSE}
|
||||
|
||||
{$ELSE}
|
||||
|
||||
, Soap.EncdDecd
|
||||
{$ENDIF};
|
||||
|
||||
{$ENDIF};
|
||||
|
||||
{ TMVCSalutationMiddleware }
|
||||
|
||||
constructor TMVCJwtAuthenticationMiddleware.Create(AMVCAuthenticationHandler
|
||||
: IMVCAuthenticationHandler;
|
||||
constructor TMVCJwtAuthenticationMiddleware.Create(AMVCAuthenticationHandler: IMVCAuthenticationHandler;
|
||||
aConfigClaims: TJWTClaimsSetup;
|
||||
aSecret: string;
|
||||
aClaimsToCheck: TJWTCheckableClaims);
|
||||
aSecret: string = 'D3lph1MVCFram3w0rk';
|
||||
aLoginURLSegment: string = '/login';
|
||||
aClaimsToCheck: TJWTCheckableClaims = [
|
||||
TJWTCheckableClaim.ExpirationTime,
|
||||
TJWTCheckableClaim.NotBefore,
|
||||
TJWTCheckableClaim.IssuedAt
|
||||
]);
|
||||
begin
|
||||
inherited Create;
|
||||
FMVCAuthenticationHandler := AMVCAuthenticationHandler;
|
||||
FSecret := aSecret;
|
||||
FLoginURLSegment := aLoginURLSegment;
|
||||
FClaimsToChecks := aClaimsToCheck;
|
||||
FSetupJWTClaims := aConfigClaims;
|
||||
end;
|
||||
@ -172,6 +190,7 @@ begin
|
||||
AControllerQualifiedClassName, AActionName, lIsAuthorized);
|
||||
if lIsAuthorized then
|
||||
begin
|
||||
Context.LoggedUser.CustomData := lJWT.CustomClaims.AsCustomData;
|
||||
Handled := False;
|
||||
end
|
||||
else
|
||||
@ -193,11 +212,12 @@ var
|
||||
lUserName: string;
|
||||
lPassword: string;
|
||||
lRoles: TList<String>;
|
||||
lSessionData: TSessionData;
|
||||
lCustomData: TMVCCustomData;
|
||||
lIsValid: Boolean;
|
||||
lJWT: TJWT;
|
||||
lPair: TPair<String, String>;
|
||||
begin
|
||||
if SameText(Context.Request.PathInfo, '/login') and (Context.Request.HTTPMethod = httpPOST) then
|
||||
if (Context.Request.HTTPMethod = httpPOST) and SameText(Context.Request.PathInfo, FLoginURLSegment) then
|
||||
begin
|
||||
lUserName := Context.Request.Headers['jwtusername'];
|
||||
lPassword := Context.Request.Headers['jwtpassword'];
|
||||
@ -212,33 +232,40 @@ begin
|
||||
// check the authorization for the requested resource
|
||||
lRoles := TList<string>.Create;
|
||||
try
|
||||
lSessionData := TSessionData.Create;
|
||||
lCustomData := TMVCCustomData.Create;
|
||||
try
|
||||
FMVCAuthenticationHandler.OnAuthentication(lUserName, lPassword,
|
||||
lRoles, lIsValid, lSessionData);
|
||||
lRoles, lIsValid, lCustomData);
|
||||
if lIsValid then
|
||||
begin
|
||||
lJWT := TJWT.Create(FSecret);
|
||||
try
|
||||
// let's user config claims and custom claims
|
||||
// CustomData becomes custom claims
|
||||
for lPair in lCustomData do
|
||||
begin
|
||||
lJWT.CustomClaims[lPair.Key] := lPair.Value;
|
||||
end;
|
||||
|
||||
// let's user config additional claims and custom claims
|
||||
FSetupJWTClaims(lJWT);
|
||||
|
||||
// these claims are mandatory and managed by the middleware
|
||||
if not lJWT.CustomClaims['username'].IsEmpty then
|
||||
raise EMVCJWTException.Create
|
||||
('Custom claim "username" is reserved and cannot be modified in the JWT setup');
|
||||
('Custom claim "username" is reserved and cannot be modified in the JWT setup nor in CustomData');
|
||||
if not lJWT.CustomClaims['roles'].IsEmpty then
|
||||
raise EMVCJWTException.Create
|
||||
('Custom claim "roles" is reserved and cannot be modified in the JWT setup');
|
||||
('Custom claim "roles" is reserved and cannot be modified in the JWT setup nor in CustomData');
|
||||
|
||||
lJWT.CustomClaims['username'] := lUserName;
|
||||
lJWT.CustomClaims['roles'] := String.Join(',', lRoles.ToArray);
|
||||
|
||||
/// / setup the current logged user from the JWT
|
||||
Context.LoggedUser.Roles.AddRange(lRoles);
|
||||
Context.LoggedUser.UserName := lJWT.CustomClaims['username'];
|
||||
Context.LoggedUser.LoggedSince := lJWT.Claims.IssuedAt;
|
||||
Context.LoggedUser.Realm := lJWT.Claims.Subject;
|
||||
// Context.LoggedUser.Roles.AddRange(lRoles);
|
||||
// Context.LoggedUser.UserName := lJWT.CustomClaims['username'];
|
||||
// Context.LoggedUser.LoggedSince := lJWT.Claims.IssuedAt;
|
||||
// Context.LoggedUser.Realm := lJWT.Claims.Subject;
|
||||
// Context.LoggedUser.CustomData :=
|
||||
/// ////////////////////////////////////////////////
|
||||
|
||||
InternalRender(TJSONObject.Create(TJSONPair.Create('token', lJWT.GetToken)),
|
||||
@ -255,7 +282,7 @@ begin
|
||||
Handled := True;
|
||||
end;
|
||||
finally
|
||||
lSessionData.Free;
|
||||
lCustomData.Free;
|
||||
end;
|
||||
finally
|
||||
lRoles.Free;
|
||||
|
@ -69,7 +69,8 @@ uses
|
||||
|
||||
type
|
||||
TDMVCSerializationType = TSerializationType;
|
||||
TSessionData = TDictionary<string, string>;
|
||||
TMVCCustomData = TDictionary<string, string>;
|
||||
TSessionData = TMVCCustomData;
|
||||
|
||||
// RTTI ATTRIBUTES
|
||||
|
||||
@ -262,10 +263,12 @@ type
|
||||
FUserName: string;
|
||||
FLoggedSince: TDateTime;
|
||||
FRealm: string;
|
||||
FCustomData: TMVCCustomData;
|
||||
procedure SetUserName(const Value: string);
|
||||
procedure SetLoggedSince(const Value: TDateTime);
|
||||
function GetIsValidLoggedUser: Boolean;
|
||||
procedure SetRealm(const Value: string);
|
||||
procedure SetCustomData(const Value: TMVCCustomData);
|
||||
|
||||
public
|
||||
procedure SaveToSession(AWebSession: TWebSession);
|
||||
@ -276,6 +279,7 @@ type
|
||||
property LoggedSince: TDateTime read FLoggedSince write SetLoggedSince;
|
||||
property IsValid: Boolean read GetIsValidLoggedUser;
|
||||
property Realm: string read FRealm write SetRealm;
|
||||
property CustomData: TMVCCustomData read FCustomData write SetCustomData;
|
||||
constructor Create; virtual;
|
||||
destructor Destroy; override;
|
||||
end;
|
||||
@ -3040,6 +3044,7 @@ end;
|
||||
constructor TUser.Create;
|
||||
begin
|
||||
inherited;
|
||||
FCustomData := nil;
|
||||
FRoles := TList<string>.Create;
|
||||
Clear;
|
||||
end;
|
||||
@ -3047,6 +3052,7 @@ end;
|
||||
destructor TUser.Destroy;
|
||||
begin
|
||||
FRoles.Free;
|
||||
FreeAndNil(FCustomData);
|
||||
inherited;
|
||||
end;
|
||||
|
||||
@ -3092,6 +3098,11 @@ begin
|
||||
ISODateTimeToString(FLoggedSince) + '$$' + FRealm + '$$' + LRoles;
|
||||
end;
|
||||
|
||||
procedure TUser.SetCustomData(const Value: TMVCCustomData);
|
||||
begin
|
||||
FCustomData := Value;
|
||||
end;
|
||||
|
||||
procedure TUser.SetLoggedSince(const Value: TDateTime);
|
||||
begin
|
||||
if FLoggedSince = 0 then
|
||||
|
@ -1,2 +1,2 @@
|
||||
const
|
||||
DMVCFRAMEWORK_VERSION = '2.1.5 (boron)';
|
||||
DMVCFRAMEWORK_VERSION = '2.1.6 (carbon)';
|
@ -215,16 +215,7 @@
|
||||
<Overwrite>true</Overwrite>
|
||||
</Platform>
|
||||
</DeployFile>
|
||||
<DeployClass Name="DependencyModule">
|
||||
<Platform Name="Win32">
|
||||
<Operation>0</Operation>
|
||||
<Extensions>.dll;.bpl</Extensions>
|
||||
</Platform>
|
||||
<Platform Name="OSX32">
|
||||
<Operation>1</Operation>
|
||||
<Extensions>.dylib</Extensions>
|
||||
</Platform>
|
||||
</DeployClass>
|
||||
<DeployClass Name="ProjectiOSDeviceResourceRules"/>
|
||||
<DeployClass Name="ProjectOSXResource">
|
||||
<Platform Name="OSX32">
|
||||
<RemoteDir>Contents\Resources</RemoteDir>
|
||||
@ -564,7 +555,16 @@
|
||||
<Operation>1</Operation>
|
||||
</Platform>
|
||||
</DeployClass>
|
||||
<DeployClass Name="ProjectiOSDeviceResourceRules"/>
|
||||
<DeployClass Name="DependencyModule">
|
||||
<Platform Name="Win32">
|
||||
<Operation>0</Operation>
|
||||
<Extensions>.dll;.bpl</Extensions>
|
||||
</Platform>
|
||||
<Platform Name="OSX32">
|
||||
<Operation>1</Operation>
|
||||
<Extensions>.dylib</Extensions>
|
||||
</Platform>
|
||||
</DeployClass>
|
||||
<ProjectRoot Platform="iOSDevice64" Name="$(PROJECTNAME).app"/>
|
||||
<ProjectRoot Platform="Win64" Name="$(PROJECTNAME)"/>
|
||||
<ProjectRoot Platform="iOSDevice32" Name="$(PROJECTNAME).app"/>
|
||||
|
Loading…
Reference in New Issue
Block a user