mirror of https://github.com/abpframework/abp.git
102 changed files with 1042 additions and 204 deletions
@ -0,0 +1,230 @@ |
|||
# Migrating from OpenIddict to IdentityServer4 Step by Step Guide |
|||
|
|||
ABP startup templates use `OpenIddict` OpenID provider from v6.0.0 by default and `IdentityServer` projects are renamed to `AuthServer` in tiered/separated solutions. Since OpenIddict is the default OpenID provider library for ABP templates since v6.0, you may want to keep using [IdentityServer4](https://github.com/IdentityServer/IdentityServer4) library, even it is **archived and no longer maintained by the owners**. ABP doesn't provide support for newer versions of IdentityServer. This guide provides layer-by-layer guidance for migrating your existing [OpenIddict](https://github.com/openiddict/openiddict-core) application to IdentityServer4. |
|||
|
|||
## IdentityServer4 Migration Steps |
|||
|
|||
Use the `abp update` command to update your existing application. See [Upgrading docs](../Upgrading.md) for more info. Apply required migrations by following the [Migration Guides](Index.md) based on your application version. |
|||
|
|||
### Domain.Shared Layer |
|||
|
|||
- In **MyApplication.Domain.Shared.csproj** replace **project reference**: |
|||
|
|||
```csharp |
|||
<PackageReference Include="Volo.Abp.OpenIddict.Domain.Shared" Version="6.0.*" /> |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
<PackageReference Include="Volo.Abp.IdentityServer.Domain.Shared" Version="6.0.*" /> |
|||
``` |
|||
|
|||
- In **MyApplicationDomainSharedModule.cs** replace usings and **module dependencies:** |
|||
|
|||
```csharp |
|||
using Volo.Abp.OpenIddict; |
|||
... |
|||
typeof(AbpOpenIddictDomainSharedModule) |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
using Volo.Abp.IdentityServer; |
|||
... |
|||
typeof(AbpIdentityServerDomainSharedModule) |
|||
``` |
|||
|
|||
### Domain Layer |
|||
|
|||
- In **MyApplication.Domain.csproj** replace **project references**: |
|||
|
|||
```csharp |
|||
<PackageReference Include="Volo.Abp.OpenIddict.Domain" Version="6.0.*" /> |
|||
<PackageReference Include="Volo.Abp.PermissionManagement.Domain.OpenIddict" Version="6.0.*" /> |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
<PackageReference Include="Volo.Abp.IdentityServer.Domain" Version="6.0.*" /> |
|||
<PackageReference Include="Volo.Abp.PermissionManagement.Domain.IdentityServer" Version="6.0.*" /> |
|||
``` |
|||
|
|||
- In **MyApplicationDomainModule.cs** replace usings and **module dependencies**: |
|||
|
|||
```csharp |
|||
using Volo.Abp.OpenIddict; |
|||
using Volo.Abp.PermissionManagement.OpenIddict; |
|||
... |
|||
typeof(AbpOpenIddictDomainModule), |
|||
typeof(AbpPermissionManagementDomainOpenIddictModule), |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
using Volo.Abp.IdentityServer; |
|||
using Volo.Abp.PermissionManagement.IdentityServer; |
|||
... |
|||
typeof(AbpIdentityServerDomainModule), |
|||
typeof(AbpPermissionManagementDomainIdentityServerModule), |
|||
``` |
|||
|
|||
#### OpenIddictDataSeedContributor |
|||
|
|||
DataSeeder is the most important part for starting the application since it seeds the initial data for both OpenID providers. |
|||
|
|||
- Create a folder named *IdentityServer* under the Domain project and copy the [IdentityServerDataSeedContributor.cs](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.Domain/IdentityServer/IdentityServerDataSeedContributor.cs) under this folder. **Rename** all the `OpenId2Ids` with your project name. |
|||
- Delete *OpenIddict* folder that contains `OpenIddictDataSeedContributor.cs` which is no longer needed. |
|||
|
|||
### EntityFrameworkCore Layer |
|||
|
|||
If you are using MongoDB, skip this step and check the *MongoDB* layer section. |
|||
|
|||
- In **MyApplication.EntityFrameworkCore.csproj** replace **project reference**: |
|||
|
|||
```csharp |
|||
<PackageReference Include="Volo.Abp.OpenIddict.EntityFrameworkCore" Version="6.0.*" /> |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
<PackageReference Include="Volo.Abp.IdentityServer.EntityFrameworkCore" Version="6.0.*" /> |
|||
``` |
|||
|
|||
- In **MyApplicationEntityFrameworkCoreModule.cs** replace usings and **module dependencies**: |
|||
|
|||
```csharp |
|||
using Volo.Abp.OpenIddict.EntityFrameworkCore; |
|||
... |
|||
typeof(AbpOpenIddictEntityFrameworkCoreModule), |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
using Volo.Abp.IdentityServer.EntityFrameworkCore; |
|||
... |
|||
typeof(AbpIdentityServerEntityFrameworkCoreModule), |
|||
``` |
|||
|
|||
- In **MyApplicationDbContext.cs** replace usings and **fluent api configurations**: |
|||
|
|||
```csharp |
|||
using Volo.Abp.OpenIddict.EntityFrameworkCore; |
|||
... |
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Include modules to your migration db context */ |
|||
|
|||
... |
|||
builder.ConfigureOpenIddict(); |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
using Volo.Abp.IdentityServer.EntityFrameworkCore; |
|||
... |
|||
using Volo.Abp.OpenIddict.EntityFrameworkCore; |
|||
... |
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Include modules to your migration db context */ |
|||
|
|||
... |
|||
builder.ConfigureIdentityServer(); |
|||
``` |
|||
|
|||
> Not: You need to create new migration after updating the fluent api. Navigate to *EntityFrameworkCore* folder and add a new migration. Ex, `dotnet ef migrations add Updated_To_IdentityServer ` |
|||
|
|||
### MongoDB Layer |
|||
|
|||
If you are using EntityFrameworkCore, skip this step and check the *EntityFrameworkCore* layer section. |
|||
|
|||
- In **MyApplication.MongoDB.csproj** replace **project reference**: |
|||
|
|||
```csharp |
|||
<PackageReference Include="Volo.Abp.OpenIddict.MongoDB" Version="6.0.*" /> |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
<PackageReference Include="Volo.Abp.IdentityServer.MongoDB" Version="6.0.*" /> |
|||
``` |
|||
|
|||
- In **MyApplicationMongoDbModule.cs** replace usings and **module dependencies**: |
|||
|
|||
```csharp |
|||
using Volo.Abp.OpenIddict.MongoDB; |
|||
... |
|||
typeof(AbpOpenIddictMongoDbModule), |
|||
``` |
|||
|
|||
with |
|||
|
|||
```csharp |
|||
using Volo.Abp.IdentityServer.MongoDB; |
|||
... |
|||
typeof(AbpIdentityServerMongoDbModule), |
|||
``` |
|||
|
|||
### DbMigrator Project |
|||
|
|||
- In `appsettings.json` **replace OpenIddict section with IdentityServer** since IdentityServerDataSeeder will be using these information for initial data seeding: |
|||
|
|||
```json |
|||
"IdentityServer": { // Rename OpenIddict to IdentityServer |
|||
"Clients ": { // Rename Applications to Clients |
|||
... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
### Test Project |
|||
|
|||
- In **MyApplicationTestBaseModule.cs** **add** the IdentityServer related using and PreConfigurations: |
|||
|
|||
```csharp |
|||
using Volo.Abp.IdentityServer; |
|||
``` |
|||
|
|||
and |
|||
|
|||
```csharp |
|||
PreConfigure<AbpIdentityServerBuilderOptions>(options => |
|||
{ |
|||
options.AddDeveloperSigningCredential = false; |
|||
}); |
|||
|
|||
PreConfigure<IIdentityServerBuilder>(identityServerBuilder => |
|||
{ |
|||
identityServerBuilder.AddDeveloperSigningCredential(false, System.Guid.NewGuid().ToString()); |
|||
}); |
|||
``` |
|||
|
|||
to `PreConfigureServices` to run authentication related unit tests. |
|||
|
|||
### UI Layer |
|||
|
|||
You can follow the migrations guides from IdentityServer to OpenIddict in **reverse order** to update your UIs. You can also check the source-code for [Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml) and [Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml.cs) files for **AuthServer** project. |
|||
|
|||
- [Angular UI Migration](OpenIddict-Angular.md) |
|||
- [MVC/Razor UI Migration](OpenIddict-Mvc.md) |
|||
- [Blazor-Server UI Migration](OpenIddict-Blazor-Server.md) |
|||
- [Blazor-Wasm UI Migration](OpenIddict-Blazor.md) |
|||
|
|||
## Source code of samples and module |
|||
|
|||
* [Open source tiered & separate auth server application migrate OpenIddict to Identity Server](https://github.com/abpframework/abp-samples/tree/master/OpenId2Ids) |
|||
* [IdentityServer module document](https://docs.abp.io/en/abp/6.0/Modules/IdentityServer) |
|||
* [IdentityServer module source code](https://github.com/abpframework/abp/tree/rel-6.0/modules/identityserver) |
|||
@ -0,0 +1,94 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json.Linq; |
|||
using Volo.Abp.Cli.Utils; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Cli.Commands.Services; |
|||
|
|||
public class SuiteAppSettingsService : ITransientDependency |
|||
{ |
|||
private const int DefaultPort = 3000; |
|||
|
|||
public CmdHelper CmdHelper { get; } |
|||
|
|||
public SuiteAppSettingsService(CmdHelper cmdHelper) |
|||
{ |
|||
CmdHelper = cmdHelper; |
|||
} |
|||
|
|||
public async Task<int> GetSuitePortAsync() |
|||
{ |
|||
return await GetSuitePortAsync(GetCurrentSuiteVersion()); |
|||
} |
|||
|
|||
public async Task<int> GetSuitePortAsync(string version) |
|||
{ |
|||
var filePath = GetFilePathOrNull(version); |
|||
|
|||
if (filePath == null) |
|||
{ |
|||
return DefaultPort; |
|||
} |
|||
|
|||
var content = File.ReadAllText(filePath); |
|||
|
|||
var contentAsJson = JObject.Parse(content); |
|||
|
|||
var url = contentAsJson["AbpSuite"]?["ApplicationUrl"]?.ToString(); |
|||
|
|||
if (url == null) |
|||
{ |
|||
return DefaultPort; |
|||
} |
|||
|
|||
return Convert.ToInt32(url.Split(":").Last()); |
|||
} |
|||
|
|||
private string GetFilePathOrNull(string version) |
|||
{ |
|||
if (version == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var path = Path.Combine( |
|||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), |
|||
".dotnet", |
|||
"tools", |
|||
".store", |
|||
"volo.abp.suite", |
|||
version, |
|||
"volo.abp.suite", |
|||
version, |
|||
"tools", |
|||
"net7.0", |
|||
"any", |
|||
"appsettings.json" |
|||
); |
|||
|
|||
if (!File.Exists(path)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return path; |
|||
} |
|||
|
|||
private string GetCurrentSuiteVersion() |
|||
{ |
|||
var dotnetToolList = CmdHelper.RunCmdAndGetOutput("dotnet tool list -g", out int exitCode); |
|||
|
|||
var suiteLine = dotnetToolList.Split(Environment.NewLine) |
|||
.FirstOrDefault(l => l.ToLower().StartsWith("volo.abp.suite ")); |
|||
|
|||
if (string.IsNullOrEmpty(suiteLine)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return suiteLine.Split(" ", StringSplitOptions.RemoveEmptyEntries)[1]; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Blogging.Admin; |
|||
|
|||
public class BloggingAdminMenuNames |
|||
{ |
|||
public const string GroupName = "BlogManagement"; |
|||
|
|||
public const string Blogs = GroupName + ".Blogs"; |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
using System.Runtime.Serialization; |
|||
using Volo.Abp; |
|||
|
|||
namespace Volo.CmsKit.Public.Web.Security.Captcha; |
|||
|
|||
public class CaptchaException : UserFriendlyException |
|||
{ |
|||
public CaptchaException(string message) : base(message) |
|||
{ |
|||
} |
|||
|
|||
public CaptchaException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import { Component } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-card-body', |
|||
template: `<div class="card-body">
|
|||
<ng-content></ng-content> |
|||
</div>`,
|
|||
}) |
|||
export class CardBodyComponent {} |
|||
@ -0,0 +1,9 @@ |
|||
import { Component } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-card-title', |
|||
template: `<div class="card-title">
|
|||
<ng-content></ng-content> |
|||
</div>`,
|
|||
}) |
|||
export class CardTitleComponent {} |
|||
@ -0,0 +1,22 @@ |
|||
import { Component, ContentChild, Input } from '@angular/core'; |
|||
import { CardBodyComponent } from './card-body.component'; |
|||
import { CardTitleComponent } from './card-title.component'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-card', |
|||
template: ` <div class="card" [ngClass]="cardClass" [ngStyle]="cardStyle">
|
|||
<ng-content *ngIf="cardTitleTemplate" select="abp-card-title"></ng-content> |
|||
<ng-content *ngIf="cardBodyTemplate" select="abp-card-body"></ng-content> |
|||
</div>`,
|
|||
}) |
|||
export class CardComponent { |
|||
@Input() cardClass: string; |
|||
|
|||
@Input() cardStyle: string; |
|||
|
|||
@ContentChild(CardBodyComponent) |
|||
cardBodyTemplate?: CardBodyComponent; |
|||
|
|||
@ContentChild(CardTitleComponent) |
|||
cardTitleTemplate?: CardTitleComponent; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { CardBodyComponent } from './card-body.component'; |
|||
import { CardTitleComponent } from './card-title.component'; |
|||
import { CardComponent } from './card.component'; |
|||
|
|||
const declarationsWithExports = [CardComponent, CardBodyComponent, CardTitleComponent]; |
|||
|
|||
@NgModule({ |
|||
declarations: [...declarationsWithExports], |
|||
imports: [CommonModule], |
|||
exports: [...declarationsWithExports], |
|||
}) |
|||
export class CardModule {} |
|||
@ -0,0 +1,4 @@ |
|||
export * from './card.module'; |
|||
export * from './card.component'; |
|||
export * from './card-body.component'; |
|||
export * from './card-title.component'; |
|||
@ -0,0 +1,45 @@ |
|||
import { AbstractNgModelComponent } from '@abp/ng.core'; |
|||
import { Component, EventEmitter, forwardRef, Injector, Input, Output } from '@angular/core'; |
|||
import { NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-checkbox', |
|||
template: ` |
|||
<div class="mb-3"> |
|||
<input |
|||
type="checkbox" |
|||
[(ngModel)]="value" |
|||
[id]="checkboxId" |
|||
[readonly]="checkboxReadonly" |
|||
[ngClass]="checkboxClass" |
|||
[ngStyle]="checkboxStyle" |
|||
(blur)="onBlur.next()" |
|||
(focus)="onFocus.next()" |
|||
> |
|||
<label *ngIf="label" [ngClass]="labelClass" [for]="checkboxId" > {{label | abpLocalization}} </label> |
|||
</div> |
|||
`,
|
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => FormCheckboxComponent), |
|||
multi: true, |
|||
}, |
|||
] |
|||
}) |
|||
export class FormCheckboxComponent extends AbstractNgModelComponent { |
|||
|
|||
@Input() label?: string; |
|||
@Input() labelClass = 'form-check-label'; |
|||
@Input() checkboxId!: string; |
|||
@Input() checkboxStyle = ''; |
|||
@Input() checkboxClass = 'form-check-input'; |
|||
@Input() checkboxReadonly = false; |
|||
@Output() onBlur = new EventEmitter<void>(); |
|||
@Output() onFocus = new EventEmitter<void>(); |
|||
|
|||
constructor(injector: Injector) { |
|||
super(injector); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
import { AbstractNgModelComponent } from '@abp/ng.core'; |
|||
import { Component, EventEmitter, forwardRef, Injector, Input, Output } from '@angular/core'; |
|||
import { NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-form-input', |
|||
template: ` |
|||
<div class="mb-3"> |
|||
<label class= *ngIf="label" [ngClass]="labelClass" [for]="inputId" > {{label | abpLocalization}} </label> |
|||
<input |
|||
type="text" |
|||
[id]="inputId" |
|||
[placeholder]="inputPlaceholder" |
|||
[readonly]="inputReadonly" |
|||
[ngClass]="inputClass" |
|||
[ngStyle]="inputStyle" |
|||
(blur)="onBlur.next()" |
|||
(focus)="onFocus.next()" |
|||
[(ngModel)]="value"> |
|||
</div> |
|||
`,
|
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => FormInputComponent), |
|||
multi: true, |
|||
}, |
|||
] |
|||
}) |
|||
export class FormInputComponent extends AbstractNgModelComponent { |
|||
@Input() inputId!: string; |
|||
@Input() inputReadonly: boolean = false; |
|||
@Input() label: string = ''; |
|||
@Input() labelClass = 'form-label'; |
|||
@Input() inputPlaceholder: string = ''; |
|||
@Input() inputType: string = 'text'; |
|||
@Input() inputStyle: string = ''; |
|||
@Input() inputClass: string = 'form-control'; |
|||
@Output() onBlur = new EventEmitter<void>(); |
|||
@Output() onFocus = new EventEmitter<void>(); |
|||
|
|||
constructor(injector: Injector) { |
|||
super(injector); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
export enum eFormComponets { |
|||
FormInputComponent = 'FormInputComponent', |
|||
FormCheckboxComponent = 'FormCheckboxComponent', |
|||
} |
|||
@ -1 +1,2 @@ |
|||
export * from './form'; |
|||
export * from './route-names'; |
|||
|
|||
@ -0,0 +1,44 @@ |
|||
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; |
|||
import { FormCheckboxComponent } from '../components/checkbox/checkbox.component'; |
|||
|
|||
describe('FormCheckboxComponent', () => { |
|||
let spectator: SpectatorHost<FormCheckboxComponent>; |
|||
|
|||
const createHost = createHostFactory(FormCheckboxComponent); |
|||
|
|||
beforeEach( |
|||
() => |
|||
(spectator = createHost( |
|||
'<abp-checkbox></abp-checkbox>', |
|||
{ |
|||
hostProps: { attributes: { autofocus: '', name: 'abp-checkbox' } }, |
|||
}, |
|||
)), |
|||
); |
|||
|
|||
it('should display the input', () => { |
|||
expect(spectator.query('input')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should equal the default classes to form-check-input', () => { |
|||
expect(spectator.query('input')).toHaveClass('form-check-input'); |
|||
}); |
|||
|
|||
it('should equal the default type to checkbox', () => { |
|||
expect(spectator.query('input')).toHaveAttribute('type', 'checkbox'); |
|||
}); |
|||
|
|||
it('should be readonly when checkboxReadonly is true', () => { |
|||
spectator.component.checkboxReadonly = true; |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('[readonly]')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should not contain readonly when checboxReadonly is false', () => { |
|||
spectator.component.checkboxReadonly = false; |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('[disabled]')).toBeFalsy(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
@ -0,0 +1,46 @@ |
|||
import { ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; |
|||
import { FormInputComponent } from '../components/form-input/form-input.component'; |
|||
|
|||
|
|||
describe('FormInputComponent', () => { |
|||
let spectator: SpectatorHost<FormInputComponent>; |
|||
|
|||
const createHost = createHostFactory(FormInputComponent); |
|||
|
|||
beforeEach( |
|||
() => |
|||
(spectator = createHost( |
|||
'<abp-form-input></abp-form-input>', |
|||
{ |
|||
hostProps: { attributes: { autofocus: '', name: 'abp-form-input' } }, |
|||
}, |
|||
)), |
|||
); |
|||
|
|||
it('should display the input', () => { |
|||
expect(spectator.query('input')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should equal the default classes to form-control', () => { |
|||
expect(spectator.query('input')).toHaveClass('form-control'); |
|||
}); |
|||
|
|||
it('should equal the default type to text', () => { |
|||
expect(spectator.query('input')).toHaveAttribute('type', 'text'); |
|||
}); |
|||
|
|||
it('should be readonly when inputReadonly is true', () => { |
|||
spectator.component.inputReadonly = true; |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('[readonly]')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should not contain readonly when inputReadonly is false', () => { |
|||
spectator.component.inputReadonly = false; |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('[disabled]')).toBeFalsy(); |
|||
}); |
|||
|
|||
}); |
|||
|
|||
@ -1,6 +0,0 @@ |
|||
module.exports = { |
|||
mappings: { |
|||
"@node_modules/flag-icon-css/css/*": "@libs/flag-icon-css/css", |
|||
"@node_modules/flag-icon-css/flags/1x1/*": "@libs/flag-icon-css/flags/1x1" |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue