mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 20:37:23 +00:00
Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
parent
b3ff359fc1
commit
a50d1408be
@ -85,6 +85,10 @@
|
||||
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="apple" />
|
||||
Apple
|
||||
</div>
|
||||
<div class="idp-table-provider-type" *ngSwitchCase="ProviderType.PROVIDER_TYPE_SAML">
|
||||
<img class="idp-logo" src="./assets/images/idp/saml-icon.svg" alt="saml" />
|
||||
SAML SP
|
||||
</div>
|
||||
<div class="idp-table-provider-type" *ngSwitchDefault>coming soon</div>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -263,6 +263,8 @@ export class IdpTableComponent implements OnInit, OnDestroy {
|
||||
return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'github', row.id];
|
||||
case ProviderType.PROVIDER_TYPE_APPLE:
|
||||
return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'apple', row.id];
|
||||
case ProviderType.PROVIDER_TYPE_SAML:
|
||||
return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'saml', row.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -196,4 +196,20 @@
|
||||
<span class="title">Active Directory / LDAP</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="item card"
|
||||
[routerLink]="
|
||||
serviceType === PolicyComponentServiceType.ADMIN
|
||||
? ['/instance', 'provider', 'saml', 'create']
|
||||
: serviceType === PolicyComponentServiceType.MGMT
|
||||
? ['/org', 'provider', 'saml', 'create']
|
||||
: []
|
||||
"
|
||||
>
|
||||
<img class="idp-logo" src="./assets/images/idp/saml-icon.svg" alt="oauth" />
|
||||
<div class="text-container">
|
||||
<span class="title">SAML SP</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -146,7 +146,6 @@ export class ProviderJWTComponent {
|
||||
req.setJwtEndpoint(this.jwtEndpoint?.value);
|
||||
req.setKeysEndpoint(this.keysEndpoint?.value);
|
||||
req.setProviderOptions(this.options);
|
||||
|
||||
this.loading = true;
|
||||
this.service
|
||||
.addJWTProvider(req)
|
||||
|
@ -0,0 +1,68 @@
|
||||
<cnsl-create-layout
|
||||
title="{{ id ? ('IDP.DETAIL.TITLE' | translate) : ('IDP.CREATE.TITLE' | translate) }}"
|
||||
(closed)="close()"
|
||||
>
|
||||
<div class="identity-provider-create-content">
|
||||
<div class="title-row">
|
||||
<img class="idp-logo" src="./assets/images/idp/saml-icon.svg" alt="saml" />
|
||||
<h1>{{ 'IDP.CREATE.SAML.TITLE' | translate }}</h1>
|
||||
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<p class="identity-provider-desc cnsl-secondary-text">
|
||||
{{ !provider ? ('IDP.CREATE.SAML.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
|
||||
</p>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="submitForm()">
|
||||
<div class="identity-provider-content">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="name" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'IDP.SAML.METADATAXML' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="metadataXml" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'IDP.SAML.METADATAURL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'IDP.SAML.BINDING' | translate }}</cnsl-label>
|
||||
<mat-select formControlName="binding">
|
||||
<mat-option *ngFor="let binding of bindingValues" [value]="binding">{{ binding }}</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<mat-checkbox formControlName="withSignedRequest">{{ 'IDP.SAML.SIGNEDREQUEST' | translate }}</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="identity-provider-optional-h-wrapper">
|
||||
<h2>{{ 'IDP.OPTIONAL' | translate }}</h2>
|
||||
<button (click)="showOptional = !showOptional" type="button" mat-icon-button>
|
||||
<mat-icon *ngIf="showOptional">keyboard_arrow_up</mat-icon
|
||||
><mat-icon *ngIf="!showOptional">keyboard_arrow_down</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="showOptional">
|
||||
<cnsl-provider-options
|
||||
[initialOptions]="provider?.config?.options"
|
||||
(optionsChanged)="options = $event"
|
||||
></cnsl-provider-options>
|
||||
</div>
|
||||
|
||||
<div class="identity-provider-create-actions">
|
||||
<button
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
class="continue-button"
|
||||
[disabled]="form.invalid || form.disabled"
|
||||
type="submit"
|
||||
>
|
||||
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
|
||||
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</cnsl-create-layout>
|
@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProviderSamlSpComponent } from './provider-saml-sp.component';
|
||||
|
||||
describe('ProviderSamlSpComponent', () => {
|
||||
let component: ProviderSamlSpComponent;
|
||||
let fixture: ComponentFixture<ProviderSamlSpComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProviderSamlSpComponent],
|
||||
});
|
||||
fixture = TestBed.createComponent(ProviderSamlSpComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,224 @@
|
||||
import { Component, Injector, Type } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { Options, Provider } from '../../../proto/generated/zitadel/idp_pb';
|
||||
import { AbstractControl, FormGroup, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
|
||||
import { ManagementService } from '../../../services/mgmt.service';
|
||||
import { AdminService } from '../../../services/admin.service';
|
||||
import { ToastService } from '../../../services/toast.service';
|
||||
import { GrpcAuthService } from '../../../services/grpc-auth.service';
|
||||
import { take } from 'rxjs';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from '../../../services/breadcrumb.service';
|
||||
import { requiredValidator } from '../../form-field/validators/validators';
|
||||
import {
|
||||
AddSAMLProviderRequest as AdminAddSAMLProviderRequest,
|
||||
GetProviderByIDRequest as AdminGetProviderByIDRequest,
|
||||
UpdateSAMLProviderRequest as AdminUpdateSAMLProviderRequest,
|
||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import {
|
||||
AddSAMLProviderRequest as MgmtAddSAMLProviderRequest,
|
||||
GetProviderByIDRequest as MgmtGetProviderByIDRequest,
|
||||
UpdateSAMLProviderRequest as MgmtUpdateSAMLProviderRequest,
|
||||
} from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import * as zitadel_idp_pb from '../../../proto/generated/zitadel/idp_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-provider-saml-sp',
|
||||
templateUrl: './provider-saml-sp.component.html',
|
||||
styleUrls: ['./provider-saml-sp.component.scss'],
|
||||
})
|
||||
export class ProviderSamlSpComponent {
|
||||
public id: string | null = '';
|
||||
public loading: boolean = false;
|
||||
public provider?: Provider.AsObject;
|
||||
public form!: FormGroup;
|
||||
public showOptional: boolean = false;
|
||||
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
|
||||
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
|
||||
private service!: ManagementService | AdminService;
|
||||
|
||||
bindingValues: string[] = Object.keys(zitadel_idp_pb.SAMLBinding);
|
||||
|
||||
constructor(
|
||||
private _location: Location,
|
||||
private toast: ToastService,
|
||||
private authService: GrpcAuthService,
|
||||
private route: ActivatedRoute,
|
||||
private injector: Injector,
|
||||
private breadcrumbService: BreadcrumbService,
|
||||
) {
|
||||
this._buildBreadcrumbs();
|
||||
this._initializeForm();
|
||||
this._checkFormPermissions();
|
||||
}
|
||||
|
||||
private _initializeForm(): void {
|
||||
this.form = new UntypedFormGroup({
|
||||
name: new UntypedFormControl('', [requiredValidator]),
|
||||
metadataXml: new UntypedFormControl('', [requiredValidator]),
|
||||
metadataUrl: new UntypedFormControl('', [requiredValidator]),
|
||||
binding: new UntypedFormControl(this.bindingValues[0], [requiredValidator]),
|
||||
withSignedRequest: new UntypedFormControl(true, [requiredValidator]),
|
||||
});
|
||||
}
|
||||
|
||||
private _checkFormPermissions(): void {
|
||||
this.authService
|
||||
.isAllowed(
|
||||
this.serviceType === PolicyComponentServiceType.ADMIN
|
||||
? ['iam.idp.write']
|
||||
: this.serviceType === PolicyComponentServiceType.MGMT
|
||||
? ['org.idp.write']
|
||||
: [],
|
||||
)
|
||||
.pipe(take(1))
|
||||
.subscribe((allowed) => {
|
||||
if (allowed) {
|
||||
this.form.enable();
|
||||
} else {
|
||||
this.form.disable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _buildBreadcrumbs(): void {
|
||||
this.route.data.pipe(take(1)).subscribe((data) => {
|
||||
this.serviceType = data['serviceType'];
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.service = this.injector.get(ManagementService as Type<ManagementService>);
|
||||
|
||||
const bread: Breadcrumb = {
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
};
|
||||
|
||||
this.breadcrumbService.setBreadcrumb([bread]);
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
this.service = this.injector.get(AdminService as Type<AdminService>);
|
||||
|
||||
const iamBread = new Breadcrumb({
|
||||
type: BreadcrumbType.ORG,
|
||||
name: 'Instance',
|
||||
routerLink: ['/instance'],
|
||||
});
|
||||
this.breadcrumbService.setBreadcrumb([iamBread]);
|
||||
break;
|
||||
}
|
||||
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
if (this.id) {
|
||||
this.getData(this.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateSAMLProvider(): void {
|
||||
if (this.provider) {
|
||||
const req =
|
||||
this.serviceType === PolicyComponentServiceType.MGMT
|
||||
? new MgmtUpdateSAMLProviderRequest()
|
||||
: new AdminUpdateSAMLProviderRequest();
|
||||
req.setId(this.provider.id);
|
||||
req.setName(this.name?.value);
|
||||
req.setMetadataUrl(this.metadataUrl?.value);
|
||||
req.setMetadataXml(this.metadataXml?.value);
|
||||
// @ts-ignore
|
||||
req.setBinding(zitadel_idp_pb.SAMLBinding[`${this.biding?.value}`]);
|
||||
req.setProviderOptions(this.options);
|
||||
|
||||
this.loading = true;
|
||||
this.service
|
||||
.updateSAMLProvider(req)
|
||||
.then((idp) => {
|
||||
setTimeout(() => {
|
||||
this.loading = false;
|
||||
this.close();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public addSAMLProvider(): void {
|
||||
const req =
|
||||
this.serviceType === PolicyComponentServiceType.MGMT
|
||||
? new MgmtAddSAMLProviderRequest()
|
||||
: new AdminAddSAMLProviderRequest();
|
||||
req.setName(this.name?.value);
|
||||
req.setMetadataUrl(this.metadataUrl?.value);
|
||||
req.setMetadataXml(this.metadataXml?.value);
|
||||
req.setProviderOptions(this.options);
|
||||
// @ts-ignore
|
||||
req.setBinding(zitadel_idp_pb.SAMLBinding[`${this.biding?.value}`]);
|
||||
req.setWithSignedRequest(this.withSignedRequest?.value);
|
||||
this.loading = true;
|
||||
this.service
|
||||
.addSAMLProvider(req)
|
||||
.then((idp) => {
|
||||
setTimeout(() => {
|
||||
this.loading = false;
|
||||
this.close();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
public submitForm(): void {
|
||||
this.provider ? this.updateSAMLProvider() : this.addSAMLProvider();
|
||||
}
|
||||
|
||||
private getData(id: string): void {
|
||||
const req =
|
||||
this.serviceType === PolicyComponentServiceType.ADMIN
|
||||
? new AdminGetProviderByIDRequest()
|
||||
: new MgmtGetProviderByIDRequest();
|
||||
req.setId(id);
|
||||
this.service
|
||||
.getProviderByID(req)
|
||||
.then((resp) => {
|
||||
this.provider = resp.idp;
|
||||
this.loading = false;
|
||||
if (this.provider?.config?.saml) {
|
||||
this.form.patchValue(this.provider.config.saml);
|
||||
this.name?.setValue(this.provider.name);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
private get name(): AbstractControl | null {
|
||||
return this.form.get('name');
|
||||
}
|
||||
|
||||
private get metadataXml(): AbstractControl | null {
|
||||
return this.form.get('metadataXml');
|
||||
}
|
||||
|
||||
private get metadataUrl(): AbstractControl | null {
|
||||
return this.form.get('metadataUrl');
|
||||
}
|
||||
|
||||
private get biding(): AbstractControl | null {
|
||||
return this.form.get('binding');
|
||||
}
|
||||
|
||||
private get withSignedRequest(): AbstractControl | null {
|
||||
return this.form.get('withSignedRequest');
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import { ProviderJWTComponent } from './provider-jwt/provider-jwt.component';
|
||||
import { ProviderLDAPComponent } from './provider-ldap/provider-ldap.component';
|
||||
import { ProviderOAuthComponent } from './provider-oauth/provider-oauth.component';
|
||||
import { ProviderOIDCComponent } from './provider-oidc/provider-oidc.component';
|
||||
import { ProviderSamlSpComponent } from './provider-saml-sp/provider-saml-sp.component';
|
||||
|
||||
const typeMap = {
|
||||
[ProviderType.PROVIDER_TYPE_AZURE_AD]: { path: 'azure-ad', component: ProviderAzureADComponent },
|
||||
@ -29,6 +30,7 @@ const typeMap = {
|
||||
[ProviderType.PROVIDER_TYPE_OIDC]: { path: 'oidc', component: ProviderOIDCComponent },
|
||||
[ProviderType.PROVIDER_TYPE_LDAP]: { path: 'ldap', component: ProviderLDAPComponent },
|
||||
[ProviderType.PROVIDER_TYPE_APPLE]: { path: 'apple', component: ProviderAppleComponent },
|
||||
[ProviderType.PROVIDER_TYPE_SAML]: { path: 'saml', component: ProviderSamlSpComponent },
|
||||
};
|
||||
|
||||
const routes: Routes = Object.entries(typeMap).map(([key, value]) => {
|
||||
|
@ -29,6 +29,7 @@ import { ProviderLDAPComponent } from './provider-ldap/provider-ldap.component';
|
||||
import { ProviderOAuthComponent } from './provider-oauth/provider-oauth.component';
|
||||
import { ProviderOIDCComponent } from './provider-oidc/provider-oidc.component';
|
||||
import { ProvidersRoutingModule } from './providers-routing.module';
|
||||
import { ProviderSamlSpComponent } from './provider-saml-sp/provider-saml-sp.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -45,6 +46,7 @@ import { ProvidersRoutingModule } from './providers-routing.module';
|
||||
ProviderOAuthComponent,
|
||||
ProviderLDAPComponent,
|
||||
ProviderAppleComponent,
|
||||
ProviderSamlSpComponent,
|
||||
],
|
||||
imports: [
|
||||
ProvidersRoutingModule,
|
||||
|
@ -40,6 +40,8 @@ import {
|
||||
AddNotificationPolicyResponse,
|
||||
AddOIDCSettingsRequest,
|
||||
AddOIDCSettingsResponse,
|
||||
AddSAMLProviderRequest,
|
||||
AddSAMLProviderResponse,
|
||||
AddSecondFactorToLoginPolicyRequest,
|
||||
AddSecondFactorToLoginPolicyResponse,
|
||||
AddSMSProviderTwilioRequest,
|
||||
@ -262,6 +264,8 @@ import {
|
||||
UpdatePasswordComplexityPolicyResponse,
|
||||
UpdatePrivacyPolicyRequest,
|
||||
UpdatePrivacyPolicyResponse,
|
||||
UpdateSAMLProviderRequest,
|
||||
UpdateSAMLProviderResponse,
|
||||
UpdateSecretGeneratorRequest,
|
||||
UpdateSecretGeneratorResponse,
|
||||
UpdateSMSProviderTwilioRequest,
|
||||
@ -1170,6 +1174,14 @@ export class AdminService {
|
||||
return this.grpcService.admin.updateJWTProvider(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public addSAMLProvider(req: AddSAMLProviderRequest): Promise<AddSAMLProviderResponse.AsObject> {
|
||||
return this.grpcService.admin.addSAMLProvider(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public updateSAMLProvider(req: UpdateSAMLProviderRequest): Promise<UpdateSAMLProviderResponse.AsObject> {
|
||||
return this.grpcService.admin.updateSAMLProvider(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public addGitHubEnterpriseServerProvider(
|
||||
req: AddGitHubEnterpriseServerProviderRequest,
|
||||
): Promise<AddGitHubEnterpriseServerProviderResponse.AsObject> {
|
||||
|
@ -83,6 +83,8 @@ import {
|
||||
AddProjectRoleResponse,
|
||||
AddSAMLAppRequest,
|
||||
AddSAMLAppResponse,
|
||||
AddSAMLProviderRequest,
|
||||
AddSAMLProviderResponse,
|
||||
AddSecondFactorToLoginPolicyRequest,
|
||||
AddSecondFactorToLoginPolicyResponse,
|
||||
AddUserGrantRequest,
|
||||
@ -511,6 +513,8 @@ import {
|
||||
UpdateProjectRoleResponse,
|
||||
UpdateSAMLAppConfigRequest,
|
||||
UpdateSAMLAppConfigResponse,
|
||||
UpdateSAMLProviderRequest,
|
||||
UpdateSAMLProviderResponse,
|
||||
UpdateUserGrantRequest,
|
||||
UpdateUserGrantResponse,
|
||||
UpdateUserNameRequest,
|
||||
@ -1037,6 +1041,14 @@ export class ManagementService {
|
||||
return this.grpcService.mgmt.updateJWTProvider(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public addSAMLProvider(req: AddSAMLProviderRequest): Promise<AddSAMLProviderResponse.AsObject> {
|
||||
return this.grpcService.mgmt.addSAMLProvider(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public updateSAMLProvider(req: UpdateSAMLProviderRequest): Promise<UpdateSAMLProviderResponse.AsObject> {
|
||||
return this.grpcService.mgmt.updateSAMLProvider(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public addGitHubEnterpriseServerProvider(
|
||||
req: AddGitHubEnterpriseServerProviderRequest,
|
||||
): Promise<AddGitHubEnterpriseServerProviderResponse.AsObject> {
|
||||
|
@ -1726,6 +1726,10 @@
|
||||
"APPLE": {
|
||||
"TITLE": "Sign in with Apple",
|
||||
"DESCRIPTION": "Enter the credentials for your Apple Provider"
|
||||
},
|
||||
"SAML": {
|
||||
"TITLE": "Sign in with Saml SP",
|
||||
"DESCRIPTION": "Enter the credentials for your SAML Provider"
|
||||
}
|
||||
},
|
||||
"DETAIL": {
|
||||
@ -1847,6 +1851,12 @@
|
||||
"UPLOADPRIVATEKEY": "Upload Private Key",
|
||||
"KEYMAXSIZEEXCEEDED": "Maximum size of 5kB exceeded."
|
||||
},
|
||||
"SAML": {
|
||||
"METADATAXML": "Metadata Xml",
|
||||
"METADATAURL": "Metadata URL",
|
||||
"BINDING": "Binding",
|
||||
"SIGNEDREQUEST": "Signed Request"
|
||||
},
|
||||
"TOAST": {
|
||||
"SAVED": "Successfully saved.",
|
||||
"REACTIVATED": "Idp reactivated.",
|
||||
|
9
console/src/assets/images/idp/saml-icon.svg
Normal file
9
console/src/assets/images/idp/saml-icon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
<g fill="#C22E33">
|
||||
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
|
||||
<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
|
||||
<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
x
Reference in New Issue
Block a user