pull:初次提交

This commit is contained in:
Yep_Q
2025-09-08 04:48:28 +08:00
parent 5c0619656d
commit f64f498365
11751 changed files with 1953723 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import { Client } from 'ldapts';
import type { ClientOptions, Entry } from 'ldapts';
import type { ICredentialDataDecryptedObject, IDataObject, Logger } from 'n8n-workflow';
export const BINARY_AD_ATTRIBUTES = ['objectGUID', 'objectSid'];
const resolveEntryBinaryAttributes = (entry: Entry): Entry => {
Object.entries(entry)
.filter(([k]) => BINARY_AD_ATTRIBUTES.includes(k))
.forEach(([k]) => {
entry[k] = (entry[k] as Buffer).toString('hex');
});
return entry;
};
export const resolveBinaryAttributes = (entries: Entry[]): void => {
entries.forEach((entry) => resolveEntryBinaryAttributes(entry));
};
export async function createLdapClient(
context: { logger: Logger },
credentials: ICredentialDataDecryptedObject,
nodeDebug?: boolean,
nodeType?: string,
nodeName?: string,
): Promise<Client> {
const protocol = credentials.connectionSecurity === 'tls' ? 'ldaps' : 'ldap';
const url = `${protocol}://${credentials.hostname}:${credentials.port}`;
const ldapOptions: ClientOptions = { url };
const tlsOptions: IDataObject = {};
if (credentials.connectionSecurity !== 'none') {
tlsOptions.rejectUnauthorized = credentials.allowUnauthorizedCerts === false;
if (credentials.caCertificate) {
tlsOptions.ca = [credentials.caCertificate as string];
}
if (credentials.connectionSecurity !== 'startTls') {
ldapOptions.tlsOptions = tlsOptions;
}
}
if (credentials.timeout) {
// Convert seconds to milliseconds
ldapOptions.timeout = (credentials.timeout as number) * 1000;
}
if (nodeDebug) {
context.logger.info(
`[${nodeType} | ${nodeName}] - LDAP Options: ${JSON.stringify(ldapOptions, null, 2)}`,
);
}
const client = new Client(ldapOptions);
if (credentials.connectionSecurity === 'startTls') {
await client.startTLS(tlsOptions);
}
return client;
}

View File

@@ -0,0 +1,19 @@
{
"node": "n8n-nodes-base.ldap",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/ldap/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.ldap/"
}
]
},
"alias": ["ad", "active directory"]
}

View File

@@ -0,0 +1,445 @@
import { Attribute, Change } from 'ldapts';
import type {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
IExecuteFunctions,
ILoadOptionsFunctions,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { BINARY_AD_ATTRIBUTES, createLdapClient, resolveBinaryAttributes } from './Helpers';
import { ldapFields } from './LdapDescription';
export class Ldap implements INodeType {
description: INodeTypeDescription = {
displayName: 'Ldap',
name: 'ldap',
icon: { light: 'file:ldap.svg', dark: 'file:ldap.dark.svg' },
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with LDAP servers',
defaults: {
name: 'LDAP',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
// eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed
name: 'ldap',
required: true,
testedBy: 'ldapConnectionTest',
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Compare',
value: 'compare',
description: 'Compare an attribute',
action: 'Compare an attribute',
},
{
name: 'Create',
value: 'create',
description: 'Create a new entry',
action: 'Create a new entry',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an entry',
action: 'Delete an entry',
},
{
name: 'Rename',
value: 'rename',
description: 'Rename the DN of an existing entry',
action: 'Rename the DN of an existing entry',
},
{
name: 'Search',
value: 'search',
description: 'Search LDAP',
action: 'Search LDAP',
},
{
name: 'Update',
value: 'update',
description: 'Update attributes',
action: 'Update attributes',
},
],
default: 'search',
},
{
displayName: 'Debug',
name: 'nodeDebug',
type: 'boolean',
isNodeSetting: true,
default: false,
noDataExpression: true,
},
...ldapFields,
],
};
methods = {
credentialTest: {
async ldapConnectionTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
const credentials = credential.data as ICredentialDataDecryptedObject;
const client = await createLdapClient(this, credentials);
try {
await client.bind(credentials.bindDN as string, credentials.bindPassword as string);
} catch (error) {
return {
status: 'Error',
message: error.message,
};
} finally {
await client.unbind();
}
return {
status: 'OK',
message: 'Connection successful!',
};
},
},
loadOptions: {
async getAttributes(this: ILoadOptionsFunctions) {
const credentials = await this.getCredentials('ldap');
const client = await createLdapClient(this, credentials);
try {
await client.bind(credentials.bindDN as string, credentials.bindPassword as string);
} catch (error) {
await client.unbind();
this.logger.error(error);
return [];
}
let results;
const baseDN = this.getNodeParameter('baseDN', 0) as string;
try {
results = await client.search(baseDN, { sizeLimit: 200, paged: false }); // should this size limit be set in credentials?
} catch (error) {
this.logger.error(error);
return [];
} finally {
await client.unbind();
}
const unique = Object.keys(Object.assign({}, ...results.searchEntries));
return unique.map((x) => ({
name: x,
value: x,
}));
},
async getObjectClasses(this: ILoadOptionsFunctions) {
const credentials = await this.getCredentials('ldap');
const client = await createLdapClient(this, credentials);
try {
await client.bind(credentials.bindDN as string, credentials.bindPassword as string);
} catch (error) {
await client.unbind();
this.logger.error(error);
return [];
}
const baseDN = this.getNodeParameter('baseDN', 0) as string;
let results;
try {
results = await client.search(baseDN, { sizeLimit: 10, paged: false }); // should this size limit be set in credentials?
} catch (error) {
this.logger.error(error);
return [];
} finally {
await client.unbind();
}
const objects = [];
for (const entry of results.searchEntries) {
if (typeof entry.objectClass === 'string') {
objects.push(entry.objectClass);
} else {
objects.push(...entry.objectClass);
}
}
const unique = [...new Set(objects)];
unique.push('custom');
const result = [];
for (const value of unique) {
if (value === 'custom') {
result.push({ name: 'custom', value: 'custom' });
} else result.push({ name: value as string, value: `(objectclass=${value})` });
}
return result;
},
async getAttributesForDn(this: ILoadOptionsFunctions) {
const credentials = await this.getCredentials('ldap');
const client = await createLdapClient(this, credentials);
try {
await client.bind(credentials.bindDN as string, credentials.bindPassword as string);
} catch (error) {
await client.unbind();
this.logger.error(error);
return [];
}
let results;
const baseDN = this.getNodeParameter('dn', 0) as string;
try {
results = await client.search(baseDN, { sizeLimit: 1, paged: false });
} catch (error) {
this.logger.error(error);
return [];
} finally {
await client.unbind();
}
const unique = Object.keys(Object.assign({}, ...results.searchEntries));
return unique.map((x) => ({
name: x,
value: x,
}));
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const nodeDebug = this.getNodeParameter('nodeDebug', 0) as boolean;
const items = this.getInputData();
const returnItems: INodeExecutionData[] = [];
if (nodeDebug) {
this.logger.info(
`[${this.getNode().type} | ${this.getNode().name}] - Starting with ${
items.length
} input items`,
);
}
const credentials = await this.getCredentials('ldap');
const client = await createLdapClient(
this,
credentials,
nodeDebug,
this.getNode().type,
this.getNode().name,
);
try {
await client.bind(credentials.bindDN as string, credentials.bindPassword as string);
} catch (error) {
delete error.cert;
await client.unbind();
if (this.continueOnFail()) {
return [
items.map((x) => {
x.json.error = error.reason || 'LDAP connection error occurred';
return x;
}),
];
} else {
throw new NodeOperationError(this.getNode(), error as Error, {});
}
}
const operation = this.getNodeParameter('operation', 0);
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
if (operation === 'compare') {
const dn = this.getNodeParameter('dn', itemIndex) as string;
const attributeId = this.getNodeParameter('id', itemIndex) as string;
const value = this.getNodeParameter('value', itemIndex, '') as string;
const res = await client.compare(dn, attributeId, value);
returnItems.push({
json: { dn, attribute: attributeId, result: res },
pairedItem: { item: itemIndex },
});
} else if (operation === 'create') {
const dn = this.getNodeParameter('dn', itemIndex) as string;
const attributeFields = this.getNodeParameter('attributes', itemIndex) as IDataObject;
const attributes: IDataObject = {};
if (Object.keys(attributeFields).length) {
//@ts-ignore
attributeFields.attribute.map((attr) => {
attributes[attr.id as string] = attr.value;
});
}
await client.add(dn, attributes as unknown as Attribute[]);
returnItems.push({
json: { dn, result: 'success' },
pairedItem: { item: itemIndex },
});
} else if (operation === 'delete') {
const dn = this.getNodeParameter('dn', itemIndex) as string;
await client.del(dn);
returnItems.push({
json: { dn, result: 'success' },
pairedItem: { item: itemIndex },
});
} else if (operation === 'rename') {
const dn = this.getNodeParameter('dn', itemIndex) as string;
const targetDn = this.getNodeParameter('targetDn', itemIndex) as string;
await client.modifyDN(dn, targetDn);
returnItems.push({
json: { dn: targetDn, result: 'success' },
pairedItem: { item: itemIndex },
});
} else if (operation === 'update') {
const dn = this.getNodeParameter('dn', itemIndex) as string;
const attributes = this.getNodeParameter('attributes', itemIndex, {}) as IDataObject;
const changes: Change[] = [];
for (const [action, attrs] of Object.entries(attributes)) {
//@ts-ignore
attrs.map((attr) =>
changes.push(
new Change({
// @ts-ignore
operation: action,
modification: new Attribute({
type: attr.id as string,
values: [attr.value],
}),
}),
),
);
}
await client.modify(dn, changes);
returnItems.push({
json: { dn, result: 'success', changes },
pairedItem: { item: itemIndex },
});
} else if (operation === 'search') {
const baseDN = this.getNodeParameter('baseDN', itemIndex) as string;
let searchFor = this.getNodeParameter('searchFor', itemIndex) as string;
const returnAll = this.getNodeParameter('returnAll', itemIndex);
const limit = this.getNodeParameter('limit', itemIndex, 0);
const options = this.getNodeParameter('options', itemIndex);
const pageSize = this.getNodeParameter(
'options.pageSize',
itemIndex,
1000,
) as IDataObject;
// Set paging settings
delete options.pageSize;
options.sizeLimit = returnAll ? 0 : limit;
if (pageSize) {
options.paged = { pageSize };
}
// Set attributes to retrieve
if (typeof options.attributes === 'string') {
options.attributes = options.attributes.split(',').map((attribute) => attribute.trim());
}
options.explicitBufferAttributes = BINARY_AD_ATTRIBUTES;
if (searchFor === 'custom') {
searchFor = this.getNodeParameter('customFilter', itemIndex) as string;
} else {
const searchText = this.getNodeParameter('searchText', itemIndex) as string;
const attribute = this.getNodeParameter('attribute', itemIndex) as string;
searchFor = `(&${searchFor}(${attribute}=${searchText}))`;
}
// Replace escaped filter special chars for ease of use
// Character ASCII value
// ---------------------------
// * 0x2a
// ( 0x28
// ) 0x29
// \ 0x5c
searchFor = searchFor.replace(/\\\\/g, '\\5c');
searchFor = searchFor.replace(/\\\*/g, '\\2a');
searchFor = searchFor.replace(/\\\(/g, '\\28');
searchFor = searchFor.replace(/\\\)/g, '\\29');
options.filter = searchFor;
if (nodeDebug) {
this.logger.info(
`[${this.getNode().type} | ${this.getNode().name}] - Search Options ${JSON.stringify(
options,
null,
2,
)}`,
);
}
const results = await client.search(baseDN, options);
// Not all LDAP servers respect the sizeLimit
if (!returnAll) {
results.searchEntries = results.searchEntries.slice(0, limit);
}
resolveBinaryAttributes(results.searchEntries);
returnItems.push.apply(
returnItems,
results.searchEntries.map((result) => ({
json: result,
pairedItem: { item: itemIndex },
})),
);
}
} catch (error) {
if (this.continueOnFail()) {
returnItems.push({ json: items[itemIndex].json, error, pairedItem: itemIndex });
} else {
await client.unbind();
if (error.context) {
error.context.itemIndex = itemIndex;
throw error;
}
throw new NodeOperationError(this.getNode(), error as Error, {
itemIndex,
});
}
}
}
if (nodeDebug) {
this.logger.info(`[${this.getNode().type} | ${this.getNode().name}] - Finished`);
}
await client.unbind();
return [returnItems];
}
}

View File

@@ -0,0 +1,456 @@
import type { INodeProperties } from 'n8n-workflow';
export const ldapFields: INodeProperties[] = [
// ----------------------------------
// Common
// ----------------------------------
{
displayName: 'DN',
name: 'dn',
type: 'string',
default: '',
placeholder: 'e.g. ou=users,dc=n8n,dc=io',
required: true,
typeOptions: {
alwaysOpenEditWindow: false,
},
displayOptions: {
show: {
operation: ['compare'],
},
},
description: 'The distinguished name of the entry to compare',
},
{
displayName: 'DN',
name: 'dn',
type: 'string',
default: '',
placeholder: 'e.g. ou=users,dc=n8n,dc=io',
required: true,
typeOptions: {
alwaysOpenEditWindow: false,
},
displayOptions: {
show: {
operation: ['create'],
},
},
description: 'The distinguished name of the entry to create',
},
{
displayName: 'DN',
name: 'dn',
type: 'string',
default: '',
placeholder: 'e.g. ou=users,dc=n8n,dc=io',
required: true,
typeOptions: {
alwaysOpenEditWindow: false,
},
displayOptions: {
show: {
operation: ['delete'],
},
},
description: 'The distinguished name of the entry to delete',
},
{
displayName: 'DN',
name: 'dn',
type: 'string',
default: '',
placeholder: 'e.g. cn=john,ou=users,dc=n8n,dc=io',
required: true,
typeOptions: {
alwaysOpenEditWindow: false,
},
displayOptions: {
show: {
operation: ['rename'],
},
},
description: 'The distinguished name of the entry to rename',
},
{
displayName: 'DN',
name: 'dn',
type: 'string',
default: '',
placeholder: 'e.g. ou=users,dc=n8n,dc=io',
required: true,
typeOptions: {
alwaysOpenEditWindow: false,
},
displayOptions: {
show: {
operation: ['modify'],
},
},
description: 'The distinguished name of the entry to modify',
},
{
displayName: 'DN',
name: 'dn',
type: 'string',
default: '',
placeholder: 'e.g. ou=users,dc=n8n,dc=io',
required: true,
typeOptions: {
alwaysOpenEditWindow: false,
},
displayOptions: {
show: {
operation: ['update'],
},
},
description: 'The distinguished name of the entry to update',
},
// ----------------------------------
// Compare
// ----------------------------------
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Attribute ID',
name: 'id',
type: 'options',
required: true,
default: [],
typeOptions: {
loadOptionsMethod: 'getAttributesForDn',
},
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description: 'The ID of the attribute to compare',
displayOptions: {
show: {
operation: ['compare'],
},
},
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value to compare',
displayOptions: {
show: {
operation: ['compare'],
},
},
},
// ----------------------------------
// Rename
// ----------------------------------
{
displayName: 'New DN',
name: 'targetDn',
type: 'string',
default: '',
placeholder: 'e.g. cn=nathan,ou=users,dc=n8n,dc=io',
required: true,
displayOptions: {
show: {
operation: ['rename'],
},
},
description: 'The new distinguished name for the entry',
},
// ----------------------------------
// Create
// ----------------------------------
{
displayName: 'Attributes',
name: 'attributes',
placeholder: 'Add Attributes',
description: 'Attributes to add to the entry',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
operation: ['create'],
},
},
default: {},
options: [
{
name: 'attribute',
displayName: 'Attribute',
values: [
{
displayName: 'Attribute ID',
name: 'id',
type: 'string',
default: '',
description: 'The ID of the attribute to add',
required: true,
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the attribute to set',
},
],
},
],
},
// ----------------------------------
// Update
// ----------------------------------
{
displayName: 'Update Attributes',
name: 'attributes',
placeholder: 'Update Attributes',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
sortable: true,
},
displayOptions: {
show: {
operation: ['update'],
},
},
description: 'Update entry attributes',
default: {},
options: [
{
name: 'add',
displayName: 'Add',
values: [
{
displayName: 'Attribute ID',
name: 'id',
type: 'string',
default: '',
description: 'The ID of the attribute to add',
required: true,
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the attribute to set',
},
],
},
{
name: 'replace',
displayName: 'Replace',
values: [
{
displayName: 'Attribute ID',
name: 'id',
type: 'string',
default: '',
description: 'The ID of the attribute to replace',
required: true,
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the attribute to replace',
},
],
},
{
name: 'delete',
displayName: 'Remove',
values: [
{
displayName: 'Attribute ID',
name: 'id',
type: 'string',
default: '',
description: 'The ID of the attribute to remove',
required: true,
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the attribute to remove',
},
],
},
],
},
// ----------------------------------
// Search
// ----------------------------------
{
displayName: 'Base DN',
name: 'baseDN',
type: 'string',
default: '',
placeholder: 'e.g. ou=users, dc=n8n, dc=io',
required: true,
displayOptions: {
show: {
operation: ['search'],
},
},
description: 'The distinguished name of the subtree to search in',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Search For',
name: 'searchFor',
type: 'options',
default: [],
typeOptions: {
loadOptionsMethod: 'getObjectClasses',
},
displayOptions: {
show: {
operation: ['search'],
},
},
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description: 'Directory object class to search for',
},
{
displayName: 'Custom Filter',
name: 'customFilter',
type: 'string',
default: '(objectclass=*)',
displayOptions: {
show: {
operation: ['search'],
searchFor: ['custom'],
},
},
description: 'Custom LDAP filter. Escape these chars * ( ) \\ with a backslash "\\".',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Attribute',
name: 'attribute',
type: 'options',
required: true,
default: [],
typeOptions: {
loadOptionsMethod: 'getAttributes',
},
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description: 'Attribute to search for',
displayOptions: {
show: {
operation: ['search'],
},
hide: {
searchFor: ['custom'],
},
},
},
{
displayName: 'Search Text',
name: 'searchText',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ['search'],
},
hide: {
searchFor: ['custom'],
},
},
description: 'Text to search for, Use * for a wildcard',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
operation: ['search'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
description: 'Max number of results to return',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
operation: ['search'],
returnAll: [false],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
operation: ['search'],
},
},
options: [
{
displayName: 'Attribute Names or IDs',
name: 'attributes',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getAttributes',
},
default: [],
description:
'Comma-separated list of attributes to return. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Page Size',
name: 'pageSize',
type: 'number',
default: 1000,
typeOptions: {
minValue: 0,
},
description:
'Maximum number of results to request at one time. Set to 0 to disable paging.',
},
{
displayName: 'Scope',
name: 'scope',
default: 'sub',
description:
'The set of entries at or below the BaseDN that may be considered potential matches',
type: 'options',
options: [
{
name: 'Base Object',
value: 'base',
},
{
name: 'Single Level',
value: 'one',
},
{
name: 'Whole Subtree',
value: 'sub',
},
],
},
],
},
];

View File

@@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.70312 21.3359C8.39453 21.3359 8.95312 20.7773 8.95312 20.0859V6.65625C8.95312 6.01563 9.47266 5.5 10.1094 5.5H28.3672C29.0078 5.5 29.5234 6.01953 29.5234 6.65625V20.1445C29.5234 20.2969 29.5508 20.4492 29.6055 20.5898C30.0977 21.8867 32.0234 21.5312 32.0234 20.1445V6.65625C32.0234 4.63672 30.3867 3 28.3672 3H10.1094C8.08984 3 6.45312 4.63672 6.45312 6.65625V20.0859C6.45312 20.7773 7.01172 21.3359 7.70312 21.3359Z" fill="white"/>
<path d="M25.8477 10.7656C25.8477 10.0742 25.2891 9.51562 24.5977 9.51562H13.8047C13.1133 9.51562 12.5547 10.0742 12.5547 10.7656C12.5547 11.457 13.1133 12.0156 13.8047 12.0156H24.5977C25.2852 12.0156 25.8477 11.457 25.8477 10.7656ZM24.5977 15.4023H13.8047C13.1133 15.4023 12.5547 15.9609 12.5547 16.6523C12.5547 17.3438 13.1133 17.9023 13.8047 17.9023H24.5977C25.2891 17.9023 25.8477 17.3438 25.8477 16.6523C25.8477 15.9609 25.2852 15.4023 24.5977 15.4023ZM1.21875 24.4961H1.14844C0.515625 24.4961 0 25.0117 0 25.6445V35.8555C0 36.4883 0.515625 37.0039 1.14844 37.0039H7.40625C8.03906 37.0039 8.55469 36.4883 8.55469 35.8555C8.55469 35.2227 8.03906 34.707 7.40625 34.707H2.36719V25.6445C2.36719 25.0117 1.85547 24.4961 1.21875 24.4961ZM19.2383 30.75C19.2383 28.7539 18.7422 27.2305 17.7734 26.1797C16.75 25.0586 15.2695 24.4961 13.293 24.4961H9.99609C9.36328 24.4961 8.84766 25.0117 8.84766 25.6445V35.8555C8.84766 36.4883 9.36328 37.0039 9.99609 37.0039H13.293C15.2695 37.0039 16.7539 36.4414 17.7734 35.3203C18.7461 34.2539 19.2383 32.7305 19.2383 30.75ZM16.0703 33.7578C15.3906 34.4258 14.1328 34.7227 13.0742 34.7227H11.2266V26.7148H13.0742C14.4219 26.7148 15.4492 27.0273 16.0703 27.6641C16.6758 28.2852 16.9805 29.0508 16.9805 30.4531C16.9805 32.1875 16.6133 33.2266 16.0703 33.7578ZM22.4922 25.2461L18.6953 35.457C18.4141 36.207 18.9727 37.0078 19.7734 37.0078H19.8672C20.3555 37.0078 20.7891 36.6992 20.9492 36.2422L21.7852 33.8711H26.0586L26.8945 36.2422C27.0586 36.7031 27.4922 37.0078 27.9766 37.0078H28.0312C28.832 37.0078 29.3867 36.207 29.1094 35.457L25.3125 25.2461C25.1445 24.7969 24.7148 24.4961 24.2344 24.4961H23.5664C23.0898 24.4961 22.6602 24.793 22.4922 25.2461ZM22.4961 31.8672L23.875 27.9375H23.9258L25.293 31.8672H22.4961ZM34.6328 24.4961H30.7383C30.1055 24.4961 29.5898 25.0117 29.5898 25.6445V35.8555C29.5898 36.4883 30.1055 37.0039 30.7383 37.0039H30.8242C31.457 37.0039 31.9727 36.4883 31.9727 35.8555V32.2031H34.5977C37.5625 32.2031 39.0469 30.9063 39.0469 28.332C39.0469 25.7734 37.5625 24.4961 34.6328 24.4961ZM36.4219 29.6484C36.0117 29.9648 35.3672 30.1406 34.4805 30.1406H31.9727V26.6016H34.4805C35.3477 26.6016 35.9961 26.7578 36.4062 27.0938C36.8164 27.4102 37.0352 27.793 37.0352 28.3359C37.0352 28.9883 36.832 29.3164 36.4219 29.6484Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.70312 21.3359C8.39453 21.3359 8.95312 20.7773 8.95312 20.0859V6.65625C8.95312 6.01563 9.47266 5.5 10.1094 5.5H28.3672C29.0078 5.5 29.5234 6.01953 29.5234 6.65625V20.1445C29.5234 20.2969 29.5508 20.4492 29.6055 20.5898C30.0977 21.8867 32.0234 21.5312 32.0234 20.1445V6.65625C32.0234 4.63672 30.3867 3 28.3672 3H10.1094C8.08984 3 6.45312 4.63672 6.45312 6.65625V20.0859C6.45312 20.7773 7.01172 21.3359 7.70312 21.3359Z" fill="black"/>
<path d="M25.8477 10.7656C25.8477 10.0742 25.2891 9.51562 24.5977 9.51562H13.8047C13.1133 9.51562 12.5547 10.0742 12.5547 10.7656C12.5547 11.457 13.1133 12.0156 13.8047 12.0156H24.5977C25.2852 12.0156 25.8477 11.457 25.8477 10.7656ZM24.5977 15.4023H13.8047C13.1133 15.4023 12.5547 15.9609 12.5547 16.6523C12.5547 17.3438 13.1133 17.9023 13.8047 17.9023H24.5977C25.2891 17.9023 25.8477 17.3438 25.8477 16.6523C25.8477 15.9609 25.2852 15.4023 24.5977 15.4023ZM1.21875 24.4961H1.14844C0.515625 24.4961 0 25.0117 0 25.6445V35.8555C0 36.4883 0.515625 37.0039 1.14844 37.0039H7.40625C8.03906 37.0039 8.55469 36.4883 8.55469 35.8555C8.55469 35.2227 8.03906 34.707 7.40625 34.707H2.36719V25.6445C2.36719 25.0117 1.85547 24.4961 1.21875 24.4961ZM19.2383 30.75C19.2383 28.7539 18.7422 27.2305 17.7734 26.1797C16.75 25.0586 15.2695 24.4961 13.293 24.4961H9.99609C9.36328 24.4961 8.84766 25.0117 8.84766 25.6445V35.8555C8.84766 36.4883 9.36328 37.0039 9.99609 37.0039H13.293C15.2695 37.0039 16.7539 36.4414 17.7734 35.3203C18.7461 34.2539 19.2383 32.7305 19.2383 30.75ZM16.0703 33.7578C15.3906 34.4258 14.1328 34.7227 13.0742 34.7227H11.2266V26.7148H13.0742C14.4219 26.7148 15.4492 27.0273 16.0703 27.6641C16.6758 28.2852 16.9805 29.0508 16.9805 30.4531C16.9805 32.1875 16.6133 33.2266 16.0703 33.7578ZM22.4922 25.2461L18.6953 35.457C18.4141 36.207 18.9727 37.0078 19.7734 37.0078H19.8672C20.3555 37.0078 20.7891 36.6992 20.9492 36.2422L21.7852 33.8711H26.0586L26.8945 36.2422C27.0586 36.7031 27.4922 37.0078 27.9766 37.0078H28.0312C28.832 37.0078 29.3867 36.207 29.1094 35.457L25.3125 25.2461C25.1445 24.7969 24.7148 24.4961 24.2344 24.4961H23.5664C23.0898 24.4961 22.6602 24.793 22.4922 25.2461ZM22.4961 31.8672L23.875 27.9375H23.9258L25.293 31.8672H22.4961ZM34.6328 24.4961H30.7383C30.1055 24.4961 29.5898 25.0117 29.5898 25.6445V35.8555C29.5898 36.4883 30.1055 37.0039 30.7383 37.0039H30.8242C31.457 37.0039 31.9727 36.4883 31.9727 35.8555V32.2031H34.5977C37.5625 32.2031 39.0469 30.9063 39.0469 28.332C39.0469 25.7734 37.5625 24.4961 34.6328 24.4961ZM36.4219 29.6484C36.0117 29.9648 35.3672 30.1406 34.4805 30.1406H31.9727V26.6016H34.4805C35.3477 26.6016 35.9961 26.7578 36.4062 27.0938C36.8164 27.4102 37.0352 27.793 37.0352 28.3359C37.0352 28.9883 36.832 29.3164 36.4219 29.6484Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB