So ya, lots of shit

This commit is contained in:
Ryahn 2025-03-19 19:56:57 -05:00
commit 6a61c8c468
4235 changed files with 688419 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.env
node_modules
config
logs
api/.env
api/node_modules
certs

130
2weekmail.fyi Normal file
View File

@ -0,0 +1,130 @@
# Roundcube Webmail
server {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
listen 80;
server_name webmail.2weekmail.fyi;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://localhost:8081; # Roundcube container port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# PostfixAdmin
server {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
listen 80;
server_name admin.2weekmail.fyi;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://localhost:8080; # PostfixAdmin container port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# API Service
server {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
listen 80;
server_name api.2weekmail.fyi;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://localhost:3000; # API container port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 300s; # Longer timeout for API calls
}
}
# Home page
server {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
listen 80;
server_name 2weekmail.fyi;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://localhost:3350; # API container port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 300s; # Longer timeout for API calls
}
}

166
2weekmail.test Normal file
View File

@ -0,0 +1,166 @@
# Roundcube Webmail
server {
listen 80;
listen 443 ssl;
server_name webmail.2weekmail.test;
ssl_certificate /etc/nginx/certs/2weekmail.test-cert.pem;
ssl_certificate_key /etc/nginx/certs/2weekmail.test-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
# Redirect HTTP to HTTPS
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
proxy_pass http://localhost:8081; # Roundcube container port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# PostfixAdmin
server {
listen 80;
listen 443 ssl;
server_name admin.2weekmail.test;
ssl_certificate /etc/nginx/certs/2weekmail.test-cert.pem;
ssl_certificate_key /etc/nginx/certs/2weekmail.test-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
# Redirect HTTP to HTTPS
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
proxy_pass http://localhost:8080; # PostfixAdmin container port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# API Service
server {
listen 80;
listen 443 ssl;
server_name api.2weekmail.test;
ssl_certificate /etc/nginx/certs/2weekmail.test-cert.pem;
ssl_certificate_key /etc/nginx/certs/2weekmail.test-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
# Redirect HTTP to HTTPS
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
proxy_pass http://localhost:3000/api/; # API container port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 300s; # Longer timeout for API calls
}
}
# Home page
server {
listen 80;
listen 443 ssl;
server_name 2weekmail.test;
ssl_certificate /etc/nginx/certs/2weekmail.test-cert.pem;
ssl_certificate_key /etc/nginx/certs/2weekmail.test-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
# Redirect HTTP to HTTPS
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
proxy_pass http://localhost:3700; # API container port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 300s; # Longer timeout for API calls
}
}

299
api/.eslintrc.json Normal file
View File

@ -0,0 +1,299 @@
{
"env": {
"commonjs": true,
"es2021": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
"accessor-pairs": "error",
"array-bracket-newline": "error",
"array-bracket-spacing": [
"error",
"never"
],
"array-callback-return": "error",
"array-element-newline": "off",
"arrow-body-style": "off",
"arrow-parens": "off",
"arrow-spacing": [
"error",
{
"after": true,
"before": true
}
],
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": "off",
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "off",
"comma-dangle": "off",
"comma-spacing": [
"error",
{
"after": true,
"before": false
}
],
"comma-style": [
"error",
"last"
],
"complexity": "error",
"computed-property-spacing": [
"error",
"never"
],
"consistent-return": "off",
"consistent-this": "error",
"curly": "off",
"default-case": "error",
"default-case-last": "error",
"default-param-last": "error",
"dot-location": [
"error",
"property"
],
"dot-notation": "off",
"eol-last": "off",
"eqeqeq": "error",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "off",
"func-style": [
"error",
"declaration",
{
"allowArrowFunctions": true
}
],
"function-call-argument-newline": [
"error",
"consistent"
],
"function-paren-newline": "off",
"generator-star-spacing": "error",
"grouped-accessor-pairs": "error",
"guard-for-in": "error",
"id-denylist": "error",
"id-length": "off",
"id-match": "error",
"implicit-arrow-linebreak": "off",
"indent": "off",
"init-declarations": "error",
"jsx-quotes": "error",
"key-spacing": "error",
"keyword-spacing": [
"error",
{
"after": true,
"before": true
}
],
"line-comment-position": "off",
"linebreak-style": [
"error",
"unix"
],
"lines-around-comment": "error",
"lines-between-class-members": [
"error",
"always"
],
"max-classes-per-file": "error",
"max-depth": "off",
"max-len": "off",
"max-lines": "off",
"max-lines-per-function": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "error",
"multiline-comment-style": "off",
"multiline-ternary": [
"error",
"never"
],
"new-parens": "error",
"newline-per-chained-call": "off",
"no-alert": "error",
"no-array-constructor": "error",
"no-await-in-loop": "off",
"no-bitwise": "error",
"no-caller": "error",
"no-confusing-arrow": "error",
"no-console": "off",
"no-constructor-return": "error",
"no-continue": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "off",
"no-empty-function": "off",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "error",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-invalid-this": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-loss-of-precision": "error",
"no-magic-numbers": "off",
"no-mixed-operators": "off",
"no-multi-assign": "error",
"no-multi-spaces": "error",
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-negated-condition": "off",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-wrappers": "error",
"no-nonoctal-decimal-escape": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-plusplus": "off",
"no-promise-executor-return": "off",
"no-proto": "error",
"no-restricted-exports": "error",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "error",
"no-return-await": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "error",
"no-tabs": "error",
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "off",
"no-undef-init": "error",
"no-undefined": "off",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unreachable-loop": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-expressions": "error",
"no-use-before-define": "off",
"no-useless-backreference": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "off",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "error",
"no-void": "error",
"no-warning-comments": "error",
"no-whitespace-before-property": "error",
"nonblock-statement-body-position": "error",
"object-curly-newline": "error",
"object-curly-spacing": "off",
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": [
"error",
"always"
],
"operator-linebreak": "error",
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "off",
"prefer-const": "off",
"prefer-destructuring": "off",
"prefer-exponentiation-operator": "error",
"prefer-named-capture-group": "off",
"prefer-numeric-literals": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "off",
"quote-props": "off",
"quotes": "off",
"radix": [
"error",
"as-needed"
],
"require-atomic-updates": "off",
"require-await": "off",
"require-unicode-regexp": "off",
"rest-spread-spacing": [
"error",
"never"
],
"semi": "off",
"semi-spacing": [
"error",
{
"after": true,
"before": false
}
],
"semi-style": [
"error",
"last"
],
"sort-imports": "error",
"sort-keys": "off",
"sort-vars": "error",
"space-before-blocks": "error",
"space-before-function-paren": "off",
"space-in-parens": [
"error",
"never"
],
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": [
"error",
"always"
],
"strict": [
"error",
"never"
],
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": [
"error",
"never"
],
"template-tag-spacing": "error",
"unicode-bom": [
"error",
"never"
],
"vars-on-top": "error",
"wrap-iife": "error",
"wrap-regex": "error",
"yield-star-spacing": "error",
"yoda": [
"error",
"never"
]
}
}

2
api/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.env
node_modules

14
api/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM node:20-bullseye-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
EXPOSE 3700
CMD ["npm", "start"]

108
api/api_server.js Normal file
View File

@ -0,0 +1,108 @@
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const path = require('path');
const bodyParser = require("body-parser");
const { engine } = require("express-handlebars");
const handlebars = require("handlebars");
const compression = require("compression");
const helmet = require("helmet");
if (process.env.NODE_ENV === "dev") {
process.removeAllListeners("warning");
}
// Routes
const apiAuthRoutes = require('./routes/apiAuth');
const webAuthRoutes = require('./routes/webAuth');
const mailboxRoutes = require('./routes/mailboxRoutes');
const domainRoutes = require('./routes/domainRoutes');
const statsRoutes = require('./routes/statsRoutes');
const app = express();
const port = process.env.PORT || 3000;
const ip = process.env.IP || '0.0.0.0';
// Middleware
app.use(bodyParser.json());
app.use(cors());
app.disable("x-powered-by");
app.set("trust proxy", "loopback");
app.set("view engine", "hbs");
app.use(helmet());
const hbsEngine = engine({
layoutsDir: path.join(__dirname, "./views/layouts"),
extname: "hbs",
defaultLayout: "main",
partialsDir: path.join(__dirname, "./views/partials"),
helpers: {
copyrightYear: () => new Date().getFullYear(),
breadcrumb: function (options) {
const url = options.data.root.currentUrl || "/";
const parts = url.split("/").filter(Boolean);
let breadcrumb = '<ol class="breadcrumb">';
breadcrumb +=
'<li class="breadcrumb-item"><a href="/">Dashboard</a></li>';
let currentPath = "";
for (let i = 0; i < parts.length; i++) {
currentPath += "/" + parts[i];
const isLast = i === parts.length - 1;
breadcrumb += '<li class="breadcrumb-item';
if (isLast) breadcrumb += ' active" aria-current="page';
breadcrumb += '">';
if (!isLast) {
breadcrumb += `<a href="${currentPath}">${
parts[i].charAt(0).toUpperCase() + parts[i].slice(1)
}</a>`;
} else {
breadcrumb += parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
}
breadcrumb += "</li>";
}
breadcrumb += "</ol>";
return new handlebars.SafeString(breadcrumb);
},
section: function (name, options) {
if (!this._sections) this._sections = {};
this._sections[name] = options.fn(this);
return null;
},
},
});
app.engine("hbs", hbsEngine);
app.disable("view cache");
app.set("views", path.join(__dirname, "./views"));
app.use(compression());
app.use("/public", express.static(path.join(__dirname, "./public")));
// Routes
app.use('/api/auth', apiAuthRoutes);
app.use('/auth', webAuthRoutes);
app.use('/api/mailboxes', mailboxRoutes);
app.use('/api/domains', domainRoutes);
app.use('/api/stats', statsRoutes);
app.get('/', (req, res) => {
res.json({ message: 'API is running' });
});
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
// Start server
app.listen(port, ip, () => {
console.log(`API server listening on port ${port}`);
});

View File

@ -0,0 +1,220 @@
const path = require('path');
const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js'));
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const { format, differenceInHours, addHours } = require('date-fns');
class AuthController extends BaseController {
constructor() {
super();
this.protected('me', 'refreshToken', 'login');
this.admin('register');
}
/**
* @swagger
* /auth/login:
* post:
* summary: Authenticate a user
* tags: [Authentication]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* description: Username or email
* password:
* type: string
* format: password
* responses:
* 200:
* description: Login successful
* content:
* application/json:
* schema:
* type: object
* properties:
* api_key:
* type: string
* description: JWT token for authentication
* 401:
* description: Invalid credentials
*/
async login(req, res) {
const { username, password } = req.body;
const user = await models.User.query().where('username', username).orWhere('email', username).first();
if (!user) {
return res.status(401).json({ error: 'Invalid username or password' });
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return res.status(401).json({ error: 'Invalid username or password' });
}
const token = jwt.sign({ id: user.id, username: user.username, is_admin: user.is_admin, email: user.email }, process.env.JWT_SECRET, { expiresIn: '6h' });
res.json({ api_key: token });
}
/**
* @swagger
* /auth/me:
* get:
* summary: Get current user details
* tags: [Authentication]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: User details
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* username:
* type: string
* email:
* type: string
* is_admin:
* type: boolean
* is_active:
* type: boolean
* created:
* type: string
* format: date-time
* modified:
* type: string
* format: date-time
* token_expiration:
* type: string
* format: date-time
* is_token_expired:
* type: boolean
* 401:
* description: Unauthorized
*/
async me(req, res) {
const user = await models.User.query().findById(req.user.id);
const token = req.headers.authorization?.split(' ')[1];
const decodedToken = token ? jwt.decode(token) : null;
const tokenExpiration = decodedToken?.exp ? new Date(decodedToken.exp * 1000) : null;
const isTokenExpired = tokenExpiration ? tokenExpiration < new Date() : true;
res.json({
id: user.id,
username: user.username,
email: user.email,
is_admin: user.is_admin,
is_active: user.is_active,
created: user.created,
modified: user.modified,
token_expiration: tokenExpiration,
is_token_expired: isTokenExpired
});
}
/**
* @swagger
* /auth/refresh-token:
* post:
* summary: Refresh authentication token
* tags: [Authentication]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Token refreshed successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* api_key:
* type: string
* description: New JWT token
* 401:
* description: Unauthorized
*/
async refreshToken(req, res) {
const user = await models.User.query().findById(req.user.id);
const token = jwt.sign({ id: user.id, username: user.username, is_admin: user.is_admin, email: user.email }, process.env.JWT_SECRET, { expiresIn: '6h' });
res.json({ api_key: token });
}
async registerView(req, res) {
res.render('auth/register');
}
async register(req, res) {
const { username, email, password } = req.body;
const invite_token = crypto.randomBytes(32).toString('hex');
const salt = await bcrypt.genSalt(10);
const passwordEncrypted = await bcrypt.hash(password, salt);
const user = await models.User.query().insert({
username,
password: passwordEncrypted,
email,
is_admin: 0,
is_active: 0,
created: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss')
});
await models.Invite.query().insert({
user_id: user.id,
token: invite_token,
expires_at: format(addHours(new Date(), 12), 'yyyy-MM-dd HH:mm:ss')
});
res.json({
message: 'User registered successfully',
invite_token
});
}
async activateView(req, res) {
res.render('auth/activate');
}
async activate(req, res) {
const { invite_token } = req.body;
const invite = await models.Invite.query().where('token', invite_token).first();
if (!invite) {
return res.status(400).json({ error: 'Invalid invite token' });
}
if (differenceInHours(new Date(), new Date(invite.expires_at)) > 0) {
return res.status(400).json({ error: 'Invite token expired' });
}
const user = await models.User.query().where('id', invite.user_id).first();
if (!user) {
return res.status(400).json({ error: 'User not found' });
}
await models.User.query().update({
is_active: 1,
modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss')
}).where('id', user.id);
res.json({
message: 'User activated successfully',
invite_token
});
}
}
module.exports = new AuthController();

View File

@ -0,0 +1,99 @@
const path = require('path');
const { authenticateToken, authenticateAdmin } = require(path.resolve(process.env.ROOT_PATH, './middleware/auth.js'));
class BaseController {
constructor() {
// Bind all methods to this to maintain context when used as route handlers
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this))
.filter(method => method !== 'constructor' && typeof this[method] === 'function');
methods.forEach(method => {
this[method] = this[method].bind(this);
});
// Store original methods before potentially wrapping them with auth
this._originalMethods = {};
methods.forEach(method => {
this._originalMethods[method] = this[method];
});
}
/**
* Protects the given methods with authentication
* @param {string|string[]} methodNames - The method names to protect
* @returns {BaseController} The current instance for method chaining
*/
protected(methodNames) {
if (!Array.isArray(methodNames)) {
methodNames = [methodNames];
}
methodNames.forEach(methodName => {
if (typeof this[methodName] !== 'function') {
throw new Error(`Method ${methodName} does not exist on ${this.constructor.name}`);
}
// Store original method if not already stored
if (!this._originalMethods[methodName]) {
this._originalMethods[methodName] = this[methodName];
}
// Replace the method with one that applies authentication first
this[methodName] = (req, res, next) => {
return authenticateToken(req, res, (err) => {
if (err) return next(err);
return this._originalMethods[methodName](req, res, next);
});
};
});
return this; // For method chaining
}
/**
* Protects the given methods with admin authentication
* @param {string|string[]} methodNames - The method names to protect. Use "*" to protect all methods.
* @returns {BaseController} The current instance for method chaining
*/
admin(methodNames) {
if (!Array.isArray(methodNames)) {
methodNames = [methodNames];
}
// If "*" is included, protect all methods
if (methodNames.includes("*")) {
const allMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(this))
.filter(method =>
method !== 'constructor' &&
method !== 'protected' &&
method !== 'admin' &&
typeof this[method] === 'function'
);
return this.admin(allMethods);
}
methodNames.forEach(methodName => {
if (typeof this[methodName] !== 'function') {
throw new Error(`Method ${methodName} does not exist on ${this.constructor.name}`);
}
// Store original method if not already stored
if (!this._originalMethods[methodName]) {
this._originalMethods[methodName] = this[methodName];
}
// Replace the method with one that applies admin authentication first
this[methodName] = (req, res, next) => {
return authenticateAdmin(req, res, (err) => {
if (err) return next(err);
return this._originalMethods[methodName](req, res, next);
});
};
});
return this; // For method chaining
}
}
module.exports = BaseController;

View File

@ -0,0 +1,296 @@
const path = require('path');
const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js'));
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
const CloudflareAPI = require('cloudflare');
class DomainController extends BaseController {
constructor() {
super();
this.protected('getAvailableDomains');
// Admin-only endpoints for managing domains
this.admin('getAllDomains', 'createDomain', 'updateDomain', 'deleteDomain');
}
/**
* @swagger
* /domains/available:
* get:
* summary: Get all available domains
* description: Retrieves a list of all active domains
* tags: [Domains]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: List of available domains
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* domain:
* type: string
* description: Domain name
* description:
* type: string
* description: Domain description
* 401:
* description: Unauthorized - Authentication required
* 500:
* description: Server error
*/
async getAvailableDomains(req, res) {
try {
const domains = await models.Domain.query()
.where('active', 1)
.select(['domain', 'description'])
.orderBy('domain');
return res.json(domains);
} catch (error) {
console.error('Error fetching available domains:', error);
return res.status(500).json({ error: 'Failed to fetch domains' });
}
}
/**
* Get all domains (admin only)
* This returns all domains including inactive ones
*/
async getAllDomains(req, res) {
try {
const domains = await models.Domain.query()
.select(['id', 'domain', 'description', 'active', 'created', 'modified'])
.orderBy('domain');
return res.json(domains);
} catch (error) {
console.error('Error fetching all domains:', error);
return res.status(500).json({ error: 'Failed to fetch domains' });
}
}
/**
* Create a new domain (admin only)
*/
async createDomain(req, res) {
try {
const { domain, description, active } = req.body;
// Validate input
if (!domain) {
return res.status(400).json({ error: 'Domain name is required' });
}
// Check if domain already exists
const existingDomain = await models.Domain.query().where('domain', domain).first();
if (existingDomain) {
return res.status(409).json({ error: 'Domain already exists' });
}
// Create the domain
const newDomain = await models.Domain.query().insert({
domain,
description: description || domain,
active: active !== undefined ? active : 1,
transport: 'smtp',
backupmx: 0,
created: new Date().toISOString().slice(0, 19).replace('T', ' '),
modified: new Date().toISOString().slice(0, 19).replace('T', ' ')
});
const dkimRecord = await generateDkimKey(domain);
return res.status(201).json({
domain: newDomain,
dnsConfiguration: {
mx: {
host: '@',
value: `10 mail.${domain}`
},
spf: {
host: '@',
type: 'TXT',
value: `v=spf1 mx a ip4:${process.env.SERVER_IP} -all`
},
dkim: dkimRecord,
dmarc: {
host: '_dmarc',
type: 'TXT',
value: `v=DMARC1; p=reject; rua=mailto:postmaster@${domain}`
},
a: {
host: 'mail',
value: process.env.SERVER_IP
}
}
});
} catch (error) {
console.error('Error creating domain:', error);
return res.status(500).json({ error: 'Failed to create domain' });
}
}
/**
* Update a domain (admin only)
*/
async updateDomain(req, res) {
try {
const { id } = req.params;
const { description, active } = req.body;
// Check if domain exists
const domain = await models.Domain.query().findById(id);
if (!domain) {
return res.status(404).json({ error: 'Domain not found' });
}
// Update the domain
const updatedDomain = await models.Domain.query().patchAndFetchById(id, {
description: description !== undefined ? description : domain.description,
active: active !== undefined ? active : domain.active,
modified: new Date().toISOString().slice(0, 19).replace('T', ' ')
});
return res.json(updatedDomain);
} catch (error) {
console.error('Error updating domain:', error);
return res.status(500).json({ error: 'Failed to update domain' });
}
}
/**
* Delete a domain (admin only)
*/
async deleteDomain(req, res) {
try {
const { id } = req.params;
// Check if domain exists
const domain = await models.Domain.query().findById(id);
if (!domain) {
return res.status(404).json({ error: 'Domain not found' });
}
// Check if domain has mailboxes
const mailboxCount = await models.Mailbox.query()
.where('domain', domain.domain)
.resultSize();
if (mailboxCount > 0) {
return res.status(409).json({
error: 'Cannot delete domain with active mailboxes',
mailboxCount
});
}
// Delete the domain
await models.Domain.query().deleteById(id);
return res.json({ message: 'Domain deleted successfully' });
} catch (error) {
console.error('Error deleting domain:', error);
return res.status(500).json({ error: 'Failed to delete domain' });
}
}
}
async function generateDkimKey(domain) {
try {
await execPromise(`docker exec mailserver_opendkim opendkim-genkey -D /etc/opendkim/keys/${domain} -d ${domain} -s mail`);
await execPromise(`docker exec mailserver_opendkim chown -R opendkim:opendkim /etc/opendkim/keys/${domain}`);
// Read the generated public key
const { stdout } = await execPromise(`docker exec mailserver_opendkim cat /etc/opendkim/keys/${domain}/mail.txt`);
// Extract the DKIM record value
const dkimMatch = stdout.match(/p=([^)]+)/);
const dkimValue = dkimMatch ? dkimMatch[1] : null;
return {
host: 'mail._domainkey',
value: `v=DKIM1; k=rsa; p=${dkimValue}`
};
} catch (error) {
console.error('Error generating DKIM key:', error);
return null;
}
}
async function configureDNS(domain) {
const cf = new CloudflareAPI({
email: process.env.CF_EMAIL,
key: process.env.CF_API_TOKEN
});
try {
// First, get the zone ID for the domain
const zones = await cf.zones.browse();
const zone = zones.result.find(z => z.name === domain);
if (!zone) {
console.error(`Zone not found for domain: ${domain}`);
return false;
}
const zoneId = zone.id;
// Add MX record
await cf.dnsRecords.add(zoneId, {
type: 'MX',
name: '@',
content: `mail.${domain}`,
priority: 10,
ttl: 3600
});
// Add SPF record
await cf.dnsRecords.add(zoneId, {
type: 'TXT',
name: '@',
content: `v=spf1 mx a ip4:${process.env.SERVER_IP} -all`,
ttl: 3600
});
// Add DKIM record
const dkimRecord = await generateDkimKey(domain);
if (dkimRecord) {
await cf.dnsRecords.add(zoneId, {
type: 'TXT',
name: 'mail._domainkey',
content: dkimRecord.value,
ttl: 3600
});
}
// Add DMARC record
await cf.dnsRecords.add(zoneId, {
type: 'TXT',
name: '_dmarc',
content: `v=DMARC1; p=reject; rua=mailto:postmaster@${domain}`,
ttl: 3600
});
// Add A record for mail subdomain
await cf.dnsRecords.add(zoneId, {
type: 'A',
name: 'mail',
content: process.env.SERVER_IP,
ttl: 3600
});
return true;
} catch (error) {
console.error('Error configuring DNS:', error);
return false;
}
}
module.exports = new DomainController();

View File

@ -0,0 +1,320 @@
const path = require('path');
const fs = require('fs').promises;
const crypto = require('crypto');
const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js'));
const { format, addDays } = require('date-fns');
const crypt = require('unix-crypt-td-js');
class MailboxController extends BaseController {
constructor() {
super();
// Public endpoints
this.protected('createTemporaryMailbox', 'getMailboxes', 'getMailbox', 'deleteMailbox');
}
/**
* Create a temporary mailbox with random username
*
* @swagger
* /mailboxes/create:
* post:
* summary: Create a temporary mailbox
* description: Creates a temporary mailbox with a random username on a random active domain
* tags: [Mailboxes]
* security:
* - bearerAuth: []
* responses:
* 201:
* description: Mailbox created successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* description: Mailbox ID
* username:
* type: string
* description: Full email address
* password:
* type: string
* description: Plain text password for the mailbox
* expires:
* type: string
* format: date-time
* description: Expiration date (14 days from creation)
* 400:
* description: No active domains found
* 500:
* description: Server error
*/
async createTemporaryMailbox(req, res) {
try {
// Validate domain exists
const randomDomain = await models.Domain.query().where('active', 1).orderByRaw('RAND()').limit(1).first();
if (!randomDomain) {
return res.status(400).json({ error: 'No active domains found' });
}
// Generate random username (local part)
const localPart = this._generateRandomUsername();
const username = `${localPart}@${randomDomain.domain}`;
const password = this._generateRandomPassword();
// Create mailbox in database
const mailbox = await models.Mailbox.query().insert({
username,
password: password.hashedPassword,
name: `Temporary Mailbox ${localPart}`,
domain: randomDomain.domain,
local_part: localPart,
quota: 102400000, // 100MB quota
maildir: `/var/mail/${randomDomain.domain}/${localPart}`,
active: 1,
created: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
expires: format(addDays(new Date(), 14), 'yyyy-MM-dd HH:mm:ss')
});
await models.Alias.query().insert({
address: username,
goto: username,
domain: randomDomain.domain,
created: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
active: 1
});
const mailData = {
id: mailbox.id,
username: mailbox.username,
password: password.password,
expires: mailbox.expires
}
// Create physical mailbox directory
try {
const mailboxPath = path.join('/var/mail', randomDomain.domain, localPart);
await fs.mkdir(mailboxPath, { recursive: true });
console.log(`Created physical mailbox at ${mailboxPath}`);
} catch (err) {
console.error(`Error creating physical mailbox: ${err.message}`);
// Continue anyway, as the mailbox might be created by the mail server
}
return res.status(201).json(mailData);
} catch (error) {
console.error('Error creating temporary mailbox:', error);
return res.status(500).json({ error: 'Failed to create mailbox' });
}
}
/**
* Get all mailboxes for the authenticated user
*
* @swagger
* /mailboxes:
* get:
* summary: Get all mailboxes
* description: Retrieves all mailboxes for the authenticated user
* tags: [Mailboxes]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: List of mailboxes
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* username:
* type: string
* domain:
* type: string
* local_part:
* type: string
* created:
* type: string
* format: date-time
* expires:
* type: string
* format: date-time
* 500:
* description: Server error
*/
async getMailboxes(req, res) {
try {
const mailboxes = await models.Mailbox.query()
.select(['id', 'username', 'domain', 'local_part', 'created', 'expires']);
return res.json(mailboxes);
} catch (error) {
console.error('Error fetching mailboxes:', error);
return res.status(500).json({ error: 'Failed to fetch mailboxes' });
}
}
/**
* Get a specific mailbox by ID
*
* @swagger
* /mailboxes/{id}:
* get:
* summary: Get mailbox by ID
* description: Retrieves a specific mailbox by its ID
* tags: [Mailboxes]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Mailbox ID
* responses:
* 200:
* description: Mailbox details
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* username:
* type: string
* domain:
* type: string
* local_part:
* type: string
* created:
* type: string
* format: date-time
* expires:
* type: string
* format: date-time
* 404:
* description: Mailbox not found
* 500:
* description: Server error
*/
async getMailbox(req, res) {
try {
const { id } = req.params;
const mailbox = await models.Mailbox.query()
.findById(id)
.select(['id', 'username', 'domain', 'local_part', 'created', 'expires']);
if (!mailbox) {
return res.status(404).json({ error: 'Mailbox not found' });
}
return res.json(mailbox);
} catch (error) {
console.error('Error fetching mailbox:', error);
return res.status(500).json({ error: 'Failed to fetch mailbox' });
}
}
/**
* Delete a mailbox
*
* @swagger
* /mailboxes/{id}:
* delete:
* summary: Delete mailbox
* description: Deletes a mailbox by ID, including physical directory
* tags: [Mailboxes]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Mailbox ID
* responses:
* 200:
* description: Mailbox deleted successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Mailbox deleted successfully
* 404:
* description: Mailbox not found
* 500:
* description: Server error
*/
async deleteMailbox(req, res) {
try {
const { id } = req.params;
const mailbox = await models.Mailbox.query().findById(id);
if (!mailbox) {
return res.status(404).json({ error: 'Mailbox not found' });
}
// Delete physical mailbox directory
try {
const mailboxPath = path.join('/var/mail', mailbox.domain, mailbox.local_part);
await fs.rm(mailboxPath, { recursive: true, force: true });
console.log(`Deleted physical mailbox at ${mailboxPath}`);
} catch (err) {
console.error(`Error deleting physical mailbox: ${err.message}`);
// Continue anyway to delete from database
}
// Delete from database
await models.Mailbox.query().deleteById(id);
return res.json({ message: 'Mailbox deleted successfully' });
} catch (error) {
console.error('Error deleting mailbox:', error);
return res.status(500).json({ error: 'Failed to delete mailbox' });
}
}
/**
* Generate a random username
* @private
*/
_generateRandomUsername() {
// Generate a random string of 8 characters
return crypto.randomBytes(4).toString('hex');
}
/**
* Generate a random password
* @private
*/
_generateRandomPassword() {
const password = crypto.randomBytes(6).toString('hex');
// Generate a salt (8 characters)
const salt = crypto.randomBytes(4).toString('hex').substring(0, 8);
// Create MD5 crypt hash in the format $1$salt$hash using the proper algorithm
const hashedPassword = crypt(password, `$1$${salt}`);
return {
password: password,
hashedPassword: hashedPassword
}
}
}
module.exports = new MailboxController();

View File

@ -0,0 +1,512 @@
const path = require('path');
const fs = require('fs').promises;
const simpleParser = require('mailparser').simpleParser;
const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js'));
class MessageController extends BaseController {
constructor() {
super();
// Protected endpoints
this.protected('getMessages', 'getMessage', 'deleteMessage');
}
/**
* Get all messages for a specific mailbox
*
* @swagger
* /mailboxes/{mailboxId}/messages:
* get:
* summary: Get all messages
* description: Retrieves all messages for a specific mailbox
* tags: [Messages]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: mailboxId
* required: true
* schema:
* type: integer
* description: Mailbox ID
* responses:
* 200:
* description: List of messages
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: string
* description: Message ID
* subject:
* type: string
* description: Email subject
* from:
* type: string
* description: Sender information
* date:
* type: string
* format: date-time
* description: Message date
* size:
* type: integer
* description: Message size in bytes
* hasAttachments:
* type: boolean
* description: Whether the message has attachments
* 404:
* description: Mailbox not found
* 500:
* description: Server error
*/
async getMessages(req, res) {
try {
const { mailboxId } = req.params;
// Verify mailbox exists
const mailbox = await models.Mailbox.query().findById(mailboxId);
if (!mailbox) {
return res.status(404).json({ error: 'Mailbox not found' });
}
// Get the physical path to the mailbox
const mailboxPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, 'new');
try {
// Check if directory exists
await fs.access(mailboxPath);
// Read all files in the directory
const files = await fs.readdir(mailboxPath);
// Process each file to extract basic email info
const messages = await Promise.all(
files.map(async (file) => {
const filePath = path.join(mailboxPath, file);
const stats = await fs.stat(filePath);
try {
// Read file content
const content = await fs.readFile(filePath, { encoding: 'utf-8' });
// Parse email content
const parsed = await simpleParser(content);
return {
id: file,
subject: parsed.subject || '(No Subject)',
from: parsed.from?.text || '(Unknown Sender)',
date: parsed.date || stats.mtime,
size: stats.size,
hasAttachments: parsed.attachments?.length > 0 || false
};
} catch (err) {
console.error(`Error parsing email ${file}:`, err);
return {
id: file,
subject: '(Error: Could not parse email)',
from: '(Unknown)',
date: stats.mtime,
size: stats.size,
hasAttachments: false,
error: true
};
}
})
);
// Sort messages by date (newest first)
messages.sort((a, b) => new Date(b.date) - new Date(a.date));
return res.json(messages);
} catch (err) {
// If directory doesn't exist or is empty
if (err.code === 'ENOENT') {
return res.json([]);
}
throw err;
}
} catch (error) {
console.error('Error fetching messages:', error);
return res.status(500).json({ error: 'Failed to fetch messages' });
}
}
/**
* Get a specific message from a mailbox
*
* @swagger
* /mailboxes/{mailboxId}/messages/{messageId}:
* get:
* summary: Get message details
* description: Retrieves a specific message from a mailbox
* tags: [Messages]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: mailboxId
* required: true
* schema:
* type: integer
* description: Mailbox ID
* - in: path
* name: messageId
* required: true
* schema:
* type: string
* description: Message ID
* responses:
* 200:
* description: Message details
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: string
* description: Message ID
* subject:
* type: string
* description: Email subject
* from:
* type: string
* description: Sender information
* to:
* type: string
* description: Recipient information
* cc:
* type: string
* description: CC recipients
* date:
* type: string
* format: date-time
* description: Message date
* textBody:
* type: string
* description: Plain text content
* htmlBody:
* type: string
* description: HTML content
* attachments:
* type: array
* description: List of attachments
* items:
* type: object
* properties:
* filename:
* type: string
* contentType:
* type: string
* size:
* type: integer
* contentId:
* type: string
* 404:
* description: Mailbox or message not found
* 500:
* description: Server error
*/
async getMessage(req, res) {
try {
const { mailboxId, messageId } = req.params;
// Verify mailbox exists
const mailbox = await models.Mailbox.query().findById(mailboxId);
if (!mailbox) {
return res.status(404).json({ error: 'Mailbox not found' });
}
// Check both new and cur directories for the message
const directories = ['new', 'cur'];
let messageContent = null;
let messageFound = false;
for (const dir of directories) {
const messagePath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir, messageId);
try {
// Check if file exists
await fs.access(messagePath);
// Read file content
const content = await fs.readFile(messagePath, { encoding: 'utf-8' });
// Parse email content
messageContent = await simpleParser(content);
messageFound = true;
// Move message from 'new' to 'cur' if it's in 'new'
if (dir === 'new') {
const curDir = path.join('/var/mail', mailbox.domain, mailbox.local_part, 'cur');
// Ensure cur directory exists
await fs.mkdir(curDir, { recursive: true });
// Move the file
const curPath = path.join(curDir, `${messageId}:2,S`); // Mark as seen
await fs.rename(messagePath, curPath);
}
break;
} catch (err) {
// Continue to next directory if file not found
if (err.code === 'ENOENT') {
continue;
}
throw err;
}
}
if (!messageFound) {
return res.status(404).json({ error: 'Message not found' });
}
// Format the response
const response = {
id: messageId,
subject: messageContent.subject || '(No Subject)',
from: messageContent.from?.text || '(Unknown Sender)',
to: messageContent.to?.text || '',
cc: messageContent.cc?.text || '',
date: messageContent.date,
textBody: messageContent.text,
htmlBody: messageContent.html,
attachments: messageContent.attachments?.map(attachment => ({
filename: attachment.filename,
contentType: attachment.contentType,
size: attachment.size,
contentId: attachment.contentId
})) || []
};
return res.json(response);
} catch (error) {
console.error('Error fetching message:', error);
return res.status(500).json({ error: 'Failed to fetch message' });
}
}
/**
* Delete a specific message from a mailbox
*
* @swagger
* /mailboxes/{mailboxId}/messages/{messageId}:
* delete:
* summary: Delete message
* description: Deletes a specific message from a mailbox
* tags: [Messages]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: mailboxId
* required: true
* schema:
* type: integer
* description: Mailbox ID
* - in: path
* name: messageId
* required: true
* schema:
* type: string
* description: Message ID
* responses:
* 200:
* description: Message deleted successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Message deleted successfully
* 404:
* description: Mailbox or message not found
* 500:
* description: Server error
*/
async deleteMessage(req, res) {
try {
const { mailboxId, messageId } = req.params;
// Verify mailbox exists
const mailbox = await models.Mailbox.query().findById(mailboxId);
if (!mailbox) {
return res.status(404).json({ error: 'Mailbox not found' });
}
// Check both new and cur directories for the message
const directories = ['new', 'cur'];
let messageFound = false;
for (const dir of directories) {
// In 'cur' directory, the message might have flags appended
const dirPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir);
try {
// Check if directory exists
await fs.access(dirPath);
// Read all files in the directory
const files = await fs.readdir(dirPath);
// Find files that start with the messageId
for (const file of files) {
if (file === messageId || file.startsWith(`${messageId}:`)) {
const filePath = path.join(dirPath, file);
await fs.unlink(filePath);
messageFound = true;
break;
}
}
if (messageFound) break;
} catch (err) {
// Continue to next directory if directory not found
if (err.code === 'ENOENT') {
continue;
}
throw err;
}
}
if (!messageFound) {
return res.status(404).json({ error: 'Message not found' });
}
return res.json({ message: 'Message deleted successfully' });
} catch (error) {
console.error('Error deleting message:', error);
return res.status(500).json({ error: 'Failed to delete message' });
}
}
/**
* Get attachment from a message
*
* @swagger
* /mailboxes/{mailboxId}/messages/{messageId}/attachments/{attachmentId}:
* get:
* summary: Get message attachment
* description: Retrieves a specific attachment from a message
* tags: [Messages]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: mailboxId
* required: true
* schema:
* type: integer
* description: Mailbox ID
* - in: path
* name: messageId
* required: true
* schema:
* type: string
* description: Message ID
* - in: path
* name: attachmentId
* required: true
* schema:
* type: string
* description: Attachment ID or filename
* responses:
* 200:
* description: Attachment file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Mailbox, message, or attachment not found
* 500:
* description: Server error
*/
async getAttachment(req, res) {
try {
const { mailboxId, messageId, attachmentId } = req.params;
// Verify mailbox exists
const mailbox = await models.Mailbox.query().findById(mailboxId);
if (!mailbox) {
return res.status(404).json({ error: 'Mailbox not found' });
}
// Check both new and cur directories for the message
const directories = ['new', 'cur'];
let messageContent = null;
let messageFound = false;
for (const dir of directories) {
// In 'cur' directory, the message might have flags appended
const dirPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir);
try {
// Check if directory exists
await fs.access(dirPath);
// Read all files in the directory
const files = await fs.readdir(dirPath);
// Find files that start with the messageId
for (const file of files) {
if (file === messageId || file.startsWith(`${messageId}:`)) {
const filePath = path.join(dirPath, file);
// Read file content
const content = await fs.readFile(filePath, { encoding: 'utf-8' });
// Parse email content
messageContent = await simpleParser(content);
messageFound = true;
break;
}
}
if (messageFound) break;
} catch (err) {
// Continue to next directory if directory not found
if (err.code === 'ENOENT') {
continue;
}
throw err;
}
}
if (!messageFound) {
return res.status(404).json({ error: 'Message not found' });
}
// Find the attachment
const attachment = messageContent.attachments?.find(
att => att.contentId === attachmentId || att.filename === attachmentId
);
if (!attachment) {
return res.status(404).json({ error: 'Attachment not found' });
}
// Set appropriate headers
res.setHeader('Content-Type', attachment.contentType);
res.setHeader('Content-Disposition', `attachment; filename="${attachment.filename}"`);
// Send the attachment content
return res.send(attachment.content);
} catch (error) {
console.error('Error fetching attachment:', error);
return res.status(500).json({ error: 'Failed to fetch attachment' });
}
}
}
module.exports = new MessageController();

View File

@ -0,0 +1,434 @@
const path = require('path');
const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js'));
const { subDays } = require('date-fns');
class StatsController extends BaseController {
constructor() {
super();
}
/**
* Get overall system statistics
*
* @swagger
* /stats/system:
* get:
* summary: Get system statistics
* description: Retrieves overall system statistics including mailbox and domain counts
* tags: [Statistics]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: System statistics
* content:
* application/json:
* schema:
* type: object
* properties:
* mailboxes:
* type: object
* properties:
* total:
* type: integer
* description: Total number of mailboxes
* active:
* type: integer
* description: Number of active mailboxes
* expiringToday:
* type: integer
* description: Number of mailboxes expiring today
* domains:
* type: object
* properties:
* total:
* type: integer
* description: Total number of domains
* active:
* type: integer
* description: Number of active domains
* creationStats:
* type: array
* description: Mailbox creation statistics for the last 7 days
* items:
* type: object
* properties:
* date:
* type: string
* format: date
* mailboxesCreated:
* type: integer
* 500:
* description: Server error
*/
async getSystemStats(req, res) {
try {
// Get total counts
const totalMailboxes = await models.Mailbox.query().count('* as count').first();
const activeMailboxes = await models.Mailbox.query()
.where('active', 1)
.count('* as count')
.first();
const expiringToday = await models.Mailbox.query()
.where('expires', '<', new Date(new Date().setHours(23, 59, 59, 999)).toISOString().slice(0, 19).replace('T', ' '))
.where('expires', '>', new Date().toISOString().slice(0, 19).replace('T', ' '))
.count('* as count')
.first();
const totalDomains = await models.Domain.query().count('* as count').first();
const activeDomains = await models.Domain.query()
.where('active', 1)
.count('* as count')
.first();
// Get creation stats for the last 7 days
const last7Days = [];
for (let i = 6; i >= 0; i--) {
const date = subDays(new Date(), i);
const formattedDate = date.toISOString().slice(0, 10);
// Get mailboxes created on this date
const mailboxesCreated = await models.Mailbox.query()
.where('created', 'like', `${formattedDate}%`)
.count('* as count')
.first();
last7Days.push({
date: formattedDate,
mailboxesCreated: parseInt(mailboxesCreated.count) || 0
});
}
return res.json({
mailboxes: {
total: parseInt(totalMailboxes.count) || 0,
active: parseInt(activeMailboxes.count) || 0,
expiringToday: parseInt(expiringToday.count) || 0
},
domains: {
total: parseInt(totalDomains.count) || 0,
active: parseInt(activeDomains.count) || 0
},
creationStats: last7Days
});
} catch (error) {
console.error('Error fetching system stats:', error);
return res.status(500).json({ error: 'Failed to fetch system statistics' });
}
}
/**
* Get mailbox statistics
*
* @swagger
* /stats/mailboxes:
* get:
* summary: Get mailbox statistics
* description: Retrieves detailed statistics about mailboxes
* tags: [Statistics]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Mailbox statistics
* content:
* application/json:
* schema:
* type: object
* properties:
* expiryStats:
* type: object
* properties:
* expired:
* type: integer
* description: Number of expired mailboxes
* expiringSoon:
* type: integer
* description: Number of mailboxes expiring within 3 days
* creationByDay:
* type: array
* description: Mailbox creation statistics for the last 30 days
* items:
* type: object
* properties:
* date:
* type: string
* format: date
* count:
* type: integer
* topDomains:
* type: array
* description: Top 10 domains by mailbox count
* items:
* type: object
* properties:
* domain:
* type: string
* count:
* type: integer
* 500:
* description: Server error
*/
async getMailboxStats(req, res) {
try {
// Get mailboxes by expiry status
const expired = await models.Mailbox.query()
.where('expires', '<', new Date().toISOString().slice(0, 19).replace('T', ' '))
.count('* as count')
.first();
const expiringSoon = await models.Mailbox.query()
.where('expires', '>', new Date().toISOString().slice(0, 19).replace('T', ' '))
.where('expires', '<', new Date(new Date().getTime() + 3 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' '))
.count('* as count')
.first();
// Get mailboxes by creation date (last 30 days)
const creationByDay = [];
for (let i = 29; i >= 0; i--) {
const date = subDays(new Date(), i);
const formattedDate = date.toISOString().slice(0, 10);
const created = await models.Mailbox.query()
.where('created', 'like', `${formattedDate}%`)
.count('* as count')
.first();
creationByDay.push({
date: formattedDate,
count: parseInt(created.count) || 0
});
}
// Get top domains by mailbox count
const topDomains = await models.Mailbox.query()
.select('domain')
.count('* as count')
.groupBy('domain')
.orderBy('count', 'desc')
.limit(10);
return res.json({
expiryStats: {
expired: parseInt(expired.count) || 0,
expiringSoon: parseInt(expiringSoon.count) || 0
},
creationByDay,
topDomains: topDomains.map(d => ({
domain: d.domain,
count: parseInt(d.count) || 0
}))
});
} catch (error) {
console.error('Error fetching mailbox stats:', error);
return res.status(500).json({ error: 'Failed to fetch mailbox statistics' });
}
}
/**
* Get domain statistics
*
* @swagger
* /stats/domains:
* get:
* summary: Get domain statistics
* description: Retrieves detailed statistics about domains
* tags: [Statistics]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Domain statistics
* content:
* application/json:
* schema:
* type: object
* properties:
* domains:
* type: object
* properties:
* total:
* type: integer
* description: Total number of domains
* active:
* type: integer
* description: Number of active domains
* inactive:
* type: integer
* description: Number of inactive domains
* topDomains:
* type: array
* description: Top 10 domains by mailbox count
* items:
* type: object
* properties:
* domain:
* type: string
* mailboxCount:
* type: integer
* domainDetails:
* type: array
* description: Detailed information about all domains
* items:
* type: object
* properties:
* domain:
* type: string
* description:
* type: string
* active:
* type: boolean
* mailboxCount:
* type: integer
* 500:
* description: Server error
*/
async getDomainStats(req, res) {
try {
// Get all domains with mailbox counts
const domains = await models.Domain.query()
.select('domain.domain', 'domain.description', 'domain.active')
.leftJoin('mailbox', 'domain.domain', 'mailbox.domain')
.groupBy('domain.domain')
.count('mailbox.username as mailboxCount');
// Get active vs inactive domains
const activeDomains = domains.filter(d => d.active === 1).length;
const inactiveDomains = domains.filter(d => d.active === 0).length;
// Get domains with most mailboxes
const topDomains = [...domains]
.sort((a, b) => parseInt(b.mailboxCount) - parseInt(a.mailboxCount))
.slice(0, 10)
.map(d => ({
domain: d.domain,
mailboxCount: parseInt(d.mailboxCount) || 0
}));
return res.json({
domains: {
total: domains.length,
active: activeDomains,
inactive: inactiveDomains
},
topDomains,
domainDetails: domains.map(d => ({
domain: d.domain,
description: d.description,
active: d.active === 1,
mailboxCount: parseInt(d.mailboxCount) || 0
}))
});
} catch (error) {
console.error('Error fetching domain stats:', error);
return res.status(500).json({ error: 'Failed to fetch domain statistics' });
}
}
/**
* Get user statistics
*
* @swagger
* /stats/users:
* get:
* summary: Get user statistics
* description: Retrieves detailed statistics about users
* tags: [Statistics]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: User statistics
* content:
* application/json:
* schema:
* type: object
* properties:
* users:
* type: object
* properties:
* total:
* type: integer
* description: Total number of users
* admin:
* type: integer
* description: Number of admin users
* active:
* type: integer
* description: Number of active users
* creationByDay:
* type: array
* description: User creation statistics for the last 30 days
* items:
* type: object
* properties:
* date:
* type: string
* format: date
* count:
* type: integer
* invites:
* type: object
* properties:
* active:
* type: integer
* description: Number of active invites
* 500:
* description: Server error
*/
async getUserStats(req, res) {
try {
// Get total users count
const totalUsers = await models.User.query().count('* as count').first();
const adminUsers = await models.User.query()
.where('is_admin', 1)
.count('* as count')
.first();
const activeUsers = await models.User.query()
.where('is_active', 1)
.count('* as count')
.first();
// Get users created in the last 30 days
const creationByDay = [];
for (let i = 29; i >= 0; i--) {
const date = subDays(new Date(), i);
const formattedDate = date.toISOString().slice(0, 10);
const created = await models.User.query()
.where('created', 'like', `${formattedDate}%`)
.count('* as count')
.first();
creationByDay.push({
date: formattedDate,
count: parseInt(created.count) || 0
});
}
// Get active invites count
const activeInvites = await models.Invite.query()
.where('expires', '>', new Date().toISOString().slice(0, 19).replace('T', ' '))
.count('* as count')
.first();
return res.json({
users: {
total: parseInt(totalUsers.count) || 0,
admin: parseInt(adminUsers.count) || 0,
active: parseInt(activeUsers.count) || 0
},
creationByDay,
invites: {
active: parseInt(activeInvites.count) || 0
}
});
} catch (error) {
console.error('Error fetching user stats:', error);
return res.status(500).json({ error: 'Failed to fetch user statistics' });
}
}
}
module.exports = new StatsController();

29
api/create_mysql_admin.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
# Variables - replace these with your desired values
ADMIN_USER="admin"
ADMIN_PASSWORD="CHANGEME"
CONTAINER_NAME="mailserver_db"
echo "Creating MySQL admin user with root-like privileges..."
# Execute MySQL commands inside the container
docker exec -it $CONTAINER_NAME mysql -uroot -p${DB_PASS} -e "
CREATE USER IF NOT EXISTS '$ADMIN_USER'@'%' IDENTIFIED BY '$ADMIN_PASSWORD';
GRANT ALL PRIVILEGES ON *.* TO '$ADMIN_USER'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
SELECT User, Host FROM mysql.user WHERE User = '$ADMIN_USER';
"
# Check if the command was successful
if [ $? -eq 0 ]; then
echo "✅ MySQL admin user '$ADMIN_USER' created successfully!"
echo "Connection details:"
echo " - Username: $ADMIN_USER"
echo " - Password: $ADMIN_PASSWORD"
echo " - Host: localhost"
echo " - Port: 3806 (mapped port)"
else
echo "❌ Failed to create MySQL admin user."
echo "Please check your container status and credentials."
fi

26
api/db/db.js Normal file
View File

@ -0,0 +1,26 @@
const { Model } = require('objection');
const Knex = require('knex');
const path = require('path');
const fs = require('fs');
require('dotenv').config();
const dbConfig = require(path.resolve(process.env.ROOT_PATH, './knexfile'))[process.env.NODE_ENV];
const knex = Knex(dbConfig);
const modelPath = path.resolve(process.env.ROOT_PATH, './db/models');
Model.knex(knex);
const db = {
knex,
Model,
models: {}
};
fs.readdirSync(modelPath)
.filter(file => file.endsWith('.js') && !file.startsWith('BaseModel'))
.forEach(file => {
const modelName = file.split('.')[0];
const ModelClass = require(path.join(modelPath, file));
db.models[modelName] = ModelClass;
});
module.exports = db;

View File

@ -0,0 +1,25 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.createTable('users', function(table) {
table.increments('id').primary();
table.string('username').notNullable();
table.string('email').notNullable();
table.string('password').notNullable();
table.boolean('is_admin').defaultTo(false);
table.boolean('is_active').defaultTo(true);
table.text('api_key').nullable();
table.datetime('created').defaultTo(knex.fn.now());
table.datetime('modified').defaultTo(knex.fn.now());
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.dropTable('users');
};

View File

@ -0,0 +1,29 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.hasTable('invites').then(function(exists) {
if (!exists) {
return knex.schema.createTable('invites', (table) => {
table.increments('id').primary();
table.integer('user_id').unsigned().references('id').inTable('users');
table.string('token').notNullable();
table.datetime('created').defaultTo(knex.fn.now());
table.datetime('expires').notNullable();
});
}
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.hasTable('invites').then(function(exists) {
if (exists) {
return knex.schema.dropTable('invites');
}
});
};

View File

@ -0,0 +1,39 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
// Check if environment is development
const env = process.env.NODE_ENV || 'development';
if (env !== 'development') {
console.warn('This migration is only meant to run in development environment');
return Promise.resolve();
}
return knex.schema.createTable('alias', function(table) {
table.string('address', 255).primary();
table.text('goto').notNullable();
table.string('domain', 255).notNullable();
table.datetime('created').notNullable().defaultTo('2000-01-01 00:00:00');
table.datetime('modified').notNullable().defaultTo('2000-01-01 00:00:00');
table.boolean('active').notNullable().defaultTo(1);
table.comment('Postfix Admin - Virtual Aliases');
table.index('domain');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
// Check if environment is development
const env = process.env.NODE_ENV || 'development';
if (env !== 'development') {
console.warn('This migration is only meant to run in development environment');
return Promise.resolve();
}
return knex.schema.dropTable('alias');
};

View File

@ -0,0 +1,40 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
const env = process.env.NODE_ENV || 'development';
if (env !== 'development') {
console.warn('This migration is only meant to run in development environment');
return Promise.resolve();
}
return knex.schema.createTable('domain', function(table) {
table.string('domain', 255).primary();
table.string('description', 255).notNullable();
table.integer('aliases').notNullable().defaultTo(0);
table.integer('mailboxes').notNullable().defaultTo(0);
table.bigInteger('maxquota').notNullable().defaultTo(0);
table.bigInteger('quota').notNullable().defaultTo(0);
table.string('transport', 255).notNullable();
table.boolean('backupmx').notNullable().defaultTo(0);
table.datetime('created').notNullable().defaultTo('2000-01-01 00:00:00');
table.datetime('modified').notNullable().defaultTo('2000-01-01 00:00:00');
table.boolean('active').notNullable().defaultTo(1);
table.integer('password_expiry').notNullable().defaultTo(0);
table.comment('Postfix Admin - Virtual Domains');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
const env = process.env.NODE_ENV || 'development';
if (env !== 'development') {
console.warn('This migration is only meant to run in development environment');
return Promise.resolve();
}
return knex.schema.dropTable('domain');
};

View File

@ -0,0 +1,44 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
const env = process.env.NODE_ENV || 'development';
if (env !== 'development') {
console.warn('This migration is only meant to run in development environment');
return Promise.resolve();
}
return knex.schema.createTable('mailbox', function(table) {
table.string('username', 255).primary();
table.string('password', 255).notNullable();
table.string('name', 255).notNullable();
table.string('maildir', 255).notNullable();
table.bigInteger('quota').notNullable().defaultTo(0);
table.string('local_part', 255).notNullable();
table.string('domain', 255).notNullable();
table.datetime('created').notNullable().defaultTo('2000-01-01 00:00:00');
table.datetime('modified').notNullable().defaultTo('2000-01-01 00:00:00');
table.boolean('active').notNullable().defaultTo(1);
table.string('phone', 30).notNullable().defaultTo('');
table.string('email_other', 255).notNullable().defaultTo('');
table.string('token', 255).notNullable().defaultTo('');
table.datetime('token_validity').notNullable().defaultTo('2000-01-01 00:00:00');
table.datetime('password_expiry').notNullable().defaultTo('2000-01-01 00:00:00');
table.comment('Postfix Admin - Virtual Mailboxes');
table.index('domain');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
const env = process.env.NODE_ENV || 'development';
if (env !== 'development') {
console.warn('This migration is only meant to run in development environment');
return Promise.resolve();
}
return knex.schema.dropTable('mailbox');
};

View File

@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('domain', (table) => {
table.tinyint('in_cloudflare').defaultTo(0);
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('domain', (table) => {
table.dropColumn('in_cloudflare');
});
};

View File

@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('mailbox', function(table) {
table.datetime('expires').notNullable().defaultTo('2000-01-01 00:00:00');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('mailbox', function(table) {
table.dropColumn('expires');
});
};

55
api/db/models/Admin.js Normal file
View File

@ -0,0 +1,55 @@
const BaseModel = require('./BaseModel');
const { Model } = require('objection');
/**
* CREATE TABLE `admin` (
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',
`modified` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',
`active` tinyint(1) NOT NULL DEFAULT 1,
`superadmin` tinyint(1) NOT NULL DEFAULT 0,
`phone` varchar(30) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`email_other` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`token` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '',
`token_validity` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci COMMENT='Postfix Admin - Virtual Admins'
*/
class Admin extends BaseModel {
static get tableName() {
return 'admin';
}
static get idColumn() {
return 'username';
}
static get jsonSchema() {
return {
type: 'object',
required: ['username', 'password'],
properties: {
username: { type: 'string', minLength: 1, maxLength: 255 },
password: { type: 'string', minLength: 1, maxLength: 255 },
created: { type: 'string', format: 'date-time' },
modified: { type: 'string', format: 'date-time' },
active: { type: 'integer', default: 1 },
superadmin: { type: 'integer', default: 0 },
phone: { type: 'string', maxLength: 30 },
email_other: { type: 'string', maxLength: 255 },
token: { type: 'string', maxLength: 255 },
token_validity: { type: 'string', format: 'date-time' },
}
};
}
static get relationMappings() {
return {};
}
}
module.exports = Admin;

46
api/db/models/Alias.js Normal file
View File

@ -0,0 +1,46 @@
const BaseModel = require('./BaseModel');
const { Model } = require('objection');
class Alias extends BaseModel {
static get tableName() {
return 'alias';
}
static get idColumn() {
return 'id';
}
static get jsonSchema() {
return {
type: 'object',
required: ['address', 'goto', 'domain'],
properties: {
id: { type: 'integer' },
address: { type: 'string', minLength: 1, maxLength: 255 },
goto: { type: 'string', minLength: 1 },
domain: { type: 'string', minLength: 1, maxLength: 255 },
created: { type: 'string', format: 'date-time' },
modified: { type: 'string', format: 'date-time' },
active: { type: 'integer', default: 1 }
}
};
}
static get relationMappings() {
const Domain = require('./Domain');
return {
domainRelation: {
relation: Model.BelongsToOneRelation,
modelClass: Domain,
join: {
from: 'alias.domain',
to: 'domain.domain'
}
}
};
}
}
module.exports = Alias;

View File

@ -0,0 +1,49 @@
const { Model } = require('objection');
const { format } = require('date-fns');
class BaseModel extends Model {
static get useLimitInFirst() {
return true;
}
$beforeInsert() {
super.$beforeInsert();
if (this.constructor.hasColumn('created')) {
this.created = this.constructor.mysqlTimestamp(false);
}
if (this.constructor.hasColumn('modified')) {
this.modified = this.constructor.mysqlTimestamp(false);
}
}
$beforeUpdate() {
super.$beforeUpdate();
if (this.constructor.hasColumn('modified')) {
this.modified = this.constructor.mysqlTimestamp(false);
}
}
static hasColumn(columnName) {
if (this.jsonSchema &&
this.jsonSchema.properties &&
this.jsonSchema.properties[columnName]
) {
return true;
}
return false;
}
static mysqlTimestamp(schemaDefinition = true) {
const formattedDate = format(new Date(), 'yyyy-MM-dd HH:mm:ss');
return schemaDefinition ? {
type: 'string',
default: formattedDate
} : formattedDate;
}
}
module.exports = BaseModel;

51
api/db/models/Domain.js Normal file
View File

@ -0,0 +1,51 @@
const BaseModel = require('./BaseModel');
const { Model } = require('objection');
class Domain extends BaseModel {
static get tableName() {
return 'domain';
}
static get idColumn() {
return 'domain';
}
static get jsonSchema() {
return {
type: 'object',
required: ['domain', 'description'],
properties: {
domain: { type: 'string', minLength: 1, maxLength: 255 },
description: { type: 'string', maxLength: 255 },
aliases: { type: 'integer', default: 0 },
mailboxes: { type: 'integer', default: 0 },
maxquota: { type: 'integer', default: 0 },
quota: { type: 'integer', default: 0 },
transport: { type: 'string', maxLength: 255 },
backupmx: { type: 'integer', default: 0 },
created: { type: 'string', format: 'date-time' },
modified: { type: 'string', format: 'date-time' },
active: { type: 'integer', default: 1 },
password_expiry: { type: 'integer', default: 0 }
}
};
}
static get relationMappings() {
const Mailbox = require('./Mailbox');
return {
mailboxes: {
relation: Model.HasManyRelation,
modelClass: Mailbox,
join: {
from: 'domain.domain',
to: 'mailbox.domain'
}
}
};
}
}
module.exports = Domain;

View File

@ -0,0 +1,56 @@
const BaseModel = require('./BaseModel');
const { Model } = require('objection');
/**
* CREATE TABLE `domain_admins` (
`username` varchar(255) NOT NULL,
`domain` varchar(255) NOT NULL,
`created` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',
`active` tinyint(1) NOT NULL DEFAULT 1,
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`),
KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci COMMENT='Postfix Admin - Domain Admins'
*/
class DomainAdmin extends BaseModel {
static get tableName() {
return 'domain_admins';
}
static get idColumn() {
return 'id';
}
static get jsonSchema() {
return {
type: 'object',
required: ['username', 'domain'],
properties: {
id: { type: 'integer' },
username: { type: 'string', minLength: 1, maxLength: 255 },
domain: { type: 'string', minLength: 1, maxLength: 255 },
created: { type: 'string', format: 'date-time' },
active: { type: 'integer', default: 1 },
}
};
}
static get relationMappings() {
const Domain = require('./Domain');
return {
mailboxes: {
relation: Model.HasManyRelation,
modelClass: Domain,
join: {
from: 'domain_admins.domain',
to: 'domain.domain'
}
}
};
}
}
module.exports = DomainAdmin;

View File

@ -0,0 +1,93 @@
const BaseModel = require('./BaseModel');
const { Model } = require('objection');
/**
* CREATE TABLE `fetchmail` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`mailbox` varchar(255) NOT NULL,
`src_server` varchar(255) NOT NULL,
`src_auth` enum('password','kerberos_v5','kerberos','kerberos_v4','gssapi','cram-md5','otp','ntlm','msn','ssh','any') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`src_user` varchar(255) NOT NULL,
`src_password` varchar(255) NOT NULL,
`src_folder` varchar(255) NOT NULL,
`poll_time` int(11) unsigned NOT NULL DEFAULT 10,
`fetchall` tinyint(1) unsigned NOT NULL DEFAULT 0,
`keep` tinyint(1) unsigned NOT NULL DEFAULT 0,
`protocol` enum('POP3','IMAP','POP2','ETRN','AUTO') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`usessl` tinyint(1) unsigned NOT NULL DEFAULT 0,
`extra_options` text DEFAULT NULL,
`returned_text` text DEFAULT NULL,
`mda` varchar(255) NOT NULL,
`date` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00',
`sslcertck` tinyint(1) NOT NULL DEFAULT 0,
`sslcertpath` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '',
`sslfingerprint` varchar(255) DEFAULT '',
`domain` varchar(255) DEFAULT '',
`active` tinyint(1) NOT NULL DEFAULT 0,
`created` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00',
`modified` timestamp NOT NULL DEFAULT current_timestamp(),
`src_port` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci
*/
class FetchMail extends BaseModel {
static get tableName() {
return 'fetchmail';
}
static get idColumn() {
return 'id';
}
static get jsonSchema() {
return {
type: 'object',
required: ['mailbox', 'src_server', 'src_user', 'src_password', 'src_folder', 'mda', 'date'],
properties: {
id: { type: 'integer' },
mailbox: { type: 'string', minLength: 1, maxLength: 255 },
src_server: { type: 'string', minLength: 1, maxLength: 255 },
src_auth: { type: 'string', maxLength: 255 },
src_user: { type: 'string', minLength: 1, maxLength: 255 },
src_password: { type: 'string', minLength: 1, maxLength: 255 },
src_folder: { type: 'string', minLength: 1, maxLength: 255 },
poll_time: { type: 'integer', default: 10 },
fetchall: { type: 'integer', default: 0 },
keep: { type: 'integer', default: 0 },
protocol: { type: 'string', maxLength: 255 },
usessl: { type: 'integer', default: 0 },
extra_options: { type: 'string', maxLength: 255 },
returned_text: { type: 'string', maxLength: 255 },
mda: { type: 'string', maxLength: 255 },
date: { type: 'string', format: 'date-time' },
sslcertck: { type: 'integer', default: 0 },
sslcertpath: { type: 'string', maxLength: 255 },
sslfingerprint: { type: 'string', maxLength: 255 },
domain: { type: 'string', maxLength: 255 },
active: { type: 'integer', default: 0 },
created: { type: 'string', format: 'date-time' },
modified: { type: 'string', format: 'date-time' },
src_port: { type: 'integer', default: 0 }
}
};
}
static get relationMappings() {
const Mailbox = require('./Mailbox');
return {
mailboxes: {
relation: Model.HasManyRelation,
modelClass: Mailbox,
join: {
from: 'fetchmail.mailbox',
to: 'mailbox.username'
}
}
};
}
}
module.exports = FetchMail;

44
api/db/models/Invite.js Normal file
View File

@ -0,0 +1,44 @@
const BaseModel = require('./BaseModel');
const { Model } = require('objection');
class Invite extends BaseModel {
static get tableName() {
return 'invites';
}
static get idColumn() {
return 'id';
}
static get jsonSchema() {
return {
type: 'object',
required: ['user_id', 'token', 'expires_at'],
properties: {
id: { type: 'integer' },
user_id: { type: 'string', minLength: 1, maxLength: 255 },
token: { type: 'string', minLength: 1 },
expires_at: { type: 'string', format: 'date-time' },
created_at: { type: 'string', format: 'date-time' },
}
};
}
static get relationMappings() {
const User = require('./User');
return {
domainRelation: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: 'invites.user_id',
to: 'users.id'
}
}
};
}
}
module.exports = Invite;

64
api/db/models/Mailbox.js Normal file
View File

@ -0,0 +1,64 @@
const BaseModel = require('./BaseModel');
const { Model } = require('objection');
class Mailbox extends BaseModel {
static get tableName() {
return 'mailbox';
}
static get idColumn() {
return 'username';
}
static get jsonSchema() {
return {
type: 'object',
required: ['username', 'password', 'name', 'domain', 'local_part'],
properties: {
username: { type: 'string', minLength: 1, maxLength: 255 },
password: { type: 'string', minLength: 1, maxLength: 255 },
name: { type: 'string', maxLength: 255 },
domain: { type: 'string', minLength: 1, maxLength: 255 },
maildir: { type: 'string', minLength: 1, maxLength: 255 },
local_part: { type: 'string', minLength: 1, maxLength: 255 },
quota: { type: 'integer', default: 0 },
created: { type: 'string', format: 'date-time' },
modified: { type: 'string', format: 'date-time' },
active: { type: 'integer', default: 1 },
phone: { type: 'string', maxLength: 30 },
email_other: { type: 'string', maxLength: 255 },
token: { type: 'string', maxLength: 255 },
token_validity: { type: 'string', format: 'date-time' },
password_expiry: { type: 'string', format: 'date-time' },
expires: { type: 'string', format: 'date-time' }
}
};
}
static get relationMappings() {
const Domain = require('./Domain');
const FetchMail = require('./FetchMail');
return {
domainRelation: {
relation: Model.BelongsToOneRelation,
modelClass: Domain,
join: {
from: 'mailbox.domain',
to: 'domain.domain'
}
},
fetchMailRelation: {
relation: Model.HasManyRelation,
modelClass: FetchMail,
join: {
from: 'mailbox.username',
to: 'fetchmail.mailbox'
}
}
};
}
}
module.exports = Mailbox;

31
api/db/models/User.js Normal file
View File

@ -0,0 +1,31 @@
const BaseModel = require('./BaseModel');
class User extends BaseModel {
static get tableName() {
return 'users';
}
static get idColumn() {
return 'id';
}
static get jsonSchema() {
return {
type: 'object',
required: ['username', 'password', 'email'],
properties: {
id: { type: 'integer' },
username: { type: 'string', minLength: 1, maxLength: 255 },
password: { type: 'string', minLength: 1 },
email: { type: 'string', minLength: 1, maxLength: 255 },
is_admin: { type: 'boolean', default: false },
is_active: { type: 'boolean', default: true },
api_key: { type: ['string', 'null'] },
created: { type: 'string', format: 'date-time' },
modified: { type: 'string', format: 'date-time' }
}
};
}
}
module.exports = User;

View File

@ -0,0 +1,46 @@
const { format } = require("date-fns");
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.seed = async function(knex) {
// Deletes ALL existing entries
await knex('domain').del()
const date = format(new Date(), "yyyy-MM-dd HH:mm:ss");
const domains = [
{ domain: '000000014.xyz', description: '000000014.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000016.xyz', description: '000000016.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000019.xyz', description: '000000019.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000020.xyz', description: '000000020.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000021.xyz', description: '000000021.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000023.xyz', description: '000000023.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000024.xyz', description: '000000024.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000025.xyz', description: '000000025.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000026.xyz', description: '000000026.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000027.xyz', description: '000000027.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000029.xyz', description: '000000029.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000030.xyz', description: '000000030.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000031.xyz', description: '000000031.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000032.xyz', description: '000000032.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000033.xyz', description: '000000033.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000034.xyz', description: '000000034.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000035.xyz', description: '000000035.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '000000036.xyz', description: '000000036.xyz', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '20is20butimnotgay.com', description: '20is20butimnotgay.com', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '20is20butimnotgay.fyi', description: '20is20butimnotgay.fyi', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '20is20butimnotgay.top', description: '20is20butimnotgay.top', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: '2weekmail.fyi', description: '2weekmail.fyi', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: 'bigwhitevanfbi.com', description: 'bigwhitevanfbi.com', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: 'bigwhitevanfbi.fyi', description: 'bigwhitevanfbi.fyi', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: 'bigwhitevanfbi.top', description: 'bigwhitevanfbi.top', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: 'icantreadpls.fyi', description: 'icantreadpls.fyi', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: 'icantreadpls.top', description: 'icantreadpls.top', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: 'idonthaveabig.wang', description: 'idonthaveabig.wang', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
{ domain: 'idonthaveabigwang.com', description: 'idonthaveabigwang.com', aliases: 0, mailboxes: 0, maxquota: 0, quota: 0, transport: 'virtual', backupmx: 0, active: 1, created: date, modified: date, password_expiry: 0, in_cloudflare: 0 },
];
await knex('domain').insert(domains);
};

View File

@ -0,0 +1,22 @@
const { format } = require("date-fns");
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.seed = async function(knex) {
// Deletes ALL existing entries
await knex('users').del()
await knex('users').insert([
{
username: 'admin',
password: '$2b$10$7VFZfom3uWDTQqDYoLBgt.A6wop5oM5FWfPpCwUKH45tojpz9otuW',
email: 'admin@2weekmail.fyi',
is_admin: Boolean(true),
is_active: Boolean(true),
api_key: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwidXNlcm5hbWUiOiJhZG1pbiIsImlzX2FkbWluIjp0cnVlLCJlbWFpbCI6ImFkbWluQDJ3ZWVrbWFpbC5meWkiLCJpYXQiOjE3NDIzMjY1MzQsImV4cCI6MzAwNDYzMDUzNH0.fxqHFW_vDQOHqWAjSlH6Br8rir0QU82UkC9OZkczXHE',
created: format(new Date(), "yyyy-MM-dd HH:mm:ss"),
modified: format(new Date(), "yyyy-MM-dd HH:mm:ss")
}
]);
};

53
api/knexfile.js Normal file
View File

@ -0,0 +1,53 @@
const path = require('path');
const dotenv = require('dotenv');
const rootPath = process.env.ROOT_PATH || path.resolve(__dirname, '.');
dotenv.config({ path: path.resolve(rootPath, '.env') });
const dbPath = path.resolve(rootPath, './db');
/**
* @type { Object.<string, import("knex").Knex.Config> }
*/
module.exports = {
development: {
client: 'mysql2',
connection: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME
},
pool: {
min: 2,
max: 10
},
migrations: {
directory: path.join(dbPath, 'migrations')
},
seeds: {
directory: path.join(dbPath, 'seeds')
}
},
production: {
client: 'mysql2',
connection: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME
},
pool: {
min: 2,
max: 10
},
migrations: {
directory: path.join(dbPath, 'migrations')
},
seeds: {
directory: path.join(dbPath, 'seeds')
}
}
};

44
api/middleware/auth.js Normal file
View File

@ -0,0 +1,44 @@
const jwt = require('jsonwebtoken');
const path = require('path');
const {models} = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
console.error('JWT verification error:', err.message);
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
}
async function authenticateAdmin(req, res, next) {
// Check if user is logged in via session instead of JWT
if (!req.session || !req.session.login) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const user = await models.User.query().findById(req.session.user.id);
if (!user.is_admin) {
return res.status(403).json({ error: 'Admin access required' });
}
req.user = user;
next();
} catch (error) {
console.error('Admin authentication error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
module.exports = { authenticateToken, authenticateAdmin };

4695
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
api/package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "api",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "npm install && npm run migrate && npm run seed && npm run start:servers",
"start:servers": "concurrently \"npm run start:web\" \"npm run start:api\"",
"migrate": "npx knex migrate:latest",
"seed": "npx knex seed:run",
"start:web": "node web_server.js",
"start:api": "node api_server.js"
},
"type": "commonjs",
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.8.3",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.3",
"cloudflare": "^4.2.0",
"compression": "^1.8.0",
"concurrently": "^9.1.2",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-handlebars": "^8.0.1",
"handlebars": "^4.7.8",
"helmet": "^8.1.0",
"imap": "^0.8.19",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"mailparser": "^3.7.2",
"mysql2": "^3.13.0",
"node-cron": "^3.0.3",
"nodemailer": "^6.10.0",
"objection": "^3.1.5",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"unix-crypt-td-js": "^1.1.4"
},
"devDependencies": {
"eslint": "^9.22.0"
}
}

6
api/public/css/all.min.css vendored Executable file

File diff suppressed because one or more lines are too long

6
api/public/css/bootstrap.min.css vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,646 @@
area-chart:
name: chart-area
arrow-circle-o-down:
prefix: far
name: circle-down
arrow-circle-o-left:
prefix: far
name: circle-left
arrow-circle-o-right:
prefix: far
name: circle-right
arrow-circle-o-up:
prefix: far
name: circle-up
arrows:
name: up-down-left-right
arrows-alt:
name: maximize
arrows-h:
name: left-right
arrows-v:
name: up-down
asl-interpreting:
name: hands-asl-interpreting
automobile:
name: car
bank:
name: building-columns
bar-chart:
name: chart-column
bar-chart-o:
name: chart-column
bathtub:
name: bath
battery:
name: battery-full
battery-0:
name: battery-empty
battery-1:
name: battery-quarter
battery-2:
name: battery-half
battery-3:
name: battery-three-quarters
battery-4:
name: battery-full
behance-square:
prefix: fab
name: square-behance
bitbucket-square:
prefix: fab
name: bitbucket
bitcoin:
prefix: fab
name: btc
cab:
name: taxi
calendar:
name: calendar-days
calendar-times-o:
prefix: far
name: calendar-xmark
caret-square-o-down:
prefix: far
name: square-caret-down
caret-square-o-left:
prefix: far
name: square-caret-left
caret-square-o-right:
prefix: far
name: square-caret-right
caret-square-o-up:
prefix: far
name: square-caret-up
cc:
prefix: far
name: closed-captioning
chain:
name: link
chain-broken:
name: link-slash
check-circle-o:
prefix: far
name: circle-check
check-square-o:
prefix: far
name: square-check
circle-o-notch:
name: circle-notch
circle-thin:
prefix: far
name: circle
clipboard:
name: paste
clone:
prefix: far
close:
name: xmark
cloud-download:
name: cloud-arrow-down
cloud-upload:
name: cloud-arrow-up
cny:
name: yen-sign
code-fork:
name: code-branch
commenting:
name: comment-dots
commenting-o:
prefix: far
name: comment-dots
compass:
prefix: far
compress:
name: down-left-and-up-right-to-center
copyright:
prefix: far
credit-card:
prefix: far
credit-card-alt:
name: credit-card
cut:
name: scissors
cutlery:
name: utensils
dashboard:
name: gauge-high
deafness:
name: ear-deaf
dedent:
name: outdent
diamond:
prefix: far
name: gem
dollar:
name: dollar-sign
dot-circle-o:
prefix: far
name: circle-dot
drivers-license:
name: id-card
drivers-license-o:
prefix: far
name: id-card
edit:
prefix: far
name: pen-to-square
eercast:
prefix: fab
name: sellcast
eur:
name: euro-sign
euro:
name: euro-sign
exchange:
name: right-left
expand:
name: up-right-and-down-left-from-center
external-link:
name: up-right-from-square
external-link-square:
name: square-up-right
eye:
prefix: far
eye-slash:
prefix: far
eyedropper:
name: eye-dropper
fa:
prefix: fab
name: font-awesome
facebook:
prefix: fab
name: facebook-f
facebook-f:
prefix: fab
name: facebook-f
facebook-official:
prefix: fab
name: facebook
facebook-square:
prefix: fab
name: square-facebook
feed:
name: rss
file-archive-o:
prefix: far
name: file-zipper
file-movie-o:
prefix: far
name: file-video
file-photo-o:
prefix: far
name: file-image
file-picture-o:
prefix: far
name: file-image
file-sound-o:
prefix: far
name: file-audio
file-text:
name: file-lines
file-text-o:
prefix: far
name: file-lines
file-zip-o:
prefix: far
name: file-zipper
files-o:
prefix: far
name: copy
flash:
name: bolt
floppy-o:
prefix: far
name: floppy-disk
frown-o:
prefix: far
name: face-frown
gbp:
name: sterling-sign
ge:
prefix: fab
name: empire
gear:
name: gear
gears:
name: gears
git-square:
prefix: fab
name: square-git
github-square:
prefix: fab
name: square-github
gittip:
prefix: fab
name: gratipay
glass:
name: martini-glass-empty
globe:
name: earth-americas
google-plus:
prefix: fab
name: google-plus-g
google-plus-circle:
prefix: fab
name: google-plus
google-plus-official:
prefix: fab
name: google-plus
google-plus-square:
prefix: fab
name: square-google-plus
group:
name: users
hand-grab-o:
prefix: far
name: hand-back-fist
hand-o-down:
prefix: far
name: hand-point-down
hand-o-left:
prefix: far
name: hand-point-left
hand-o-right:
prefix: far
name: hand-point-right
hand-o-up:
prefix: far
name: hand-point-up
hand-paper-o:
prefix: far
name: hand
hand-rock-o:
prefix: far
name: hand-back-fist
hand-stop-o:
prefix: far
name: hand
hard-of-hearing:
name: ear-deaf
hdd-o:
prefix: far
name: hard-drive
header:
name: heading
home:
name: house
hotel:
name: bed
hourglass-1:
name: hourglass-start
hourglass-2:
name: hourglass-half
hourglass-3:
name: hourglass-end
hourglass-o:
name: hourglass
id-badge:
prefix: far
ils:
name: shekel-sign
image:
prefix: far
name: image
inr:
name: indian-rupee-sign
institution:
name: building-columns
intersex:
name: mars-and-venus
jpy:
name: yen-sign
krw:
name: won-sign
lastfm-square:
prefix: fab
name: square-lastfm
legal:
name: gavel
level-down:
name: turn-down
level-up:
name: turn-up
life-bouy:
name: life-ring
life-buoy:
name: life-ring
life-saver:
name: life-ring
line-chart:
name: chart-line
linkedin:
prefix: fab
name: linkedin-in
linkedin-square:
prefix: fab
name: linkedin
list-alt:
prefix: far
name: rectangle-list
long-arrow-down:
name: down-long
long-arrow-left:
name: left-long
long-arrow-right:
name: right-long
long-arrow-up:
name: up-long
magic:
name: wand-magic-sparkles
mail-forward:
name: share
mail-reply:
name: reply
mail-reply-all:
name: reply-all
map-marker:
name: location-dot
meh-o:
prefix: far
name: face-meh
minus-square-o:
prefix: far
name: square-minus
mobile:
name: mobile-screen-button
mobile-phone:
name: mobile-screen-button
money:
name: money-bill-1
mortar-board:
name: graduation-cap
navicon:
name: bars
object-group:
prefix: far
object-ungroup:
prefix: far
odnoklassniki-square:
prefix: fab
name: square-odnoklassniki
pause-circle-o:
prefix: far
name: circle-pause
pencil-square:
name: square-pen
pencil-square-o:
prefix: far
name: pen-to-square
photo:
prefix: far
name: image
picture-o:
prefix: far
name: image
pie-chart:
name: chart-pie
pinterest-square:
prefix: fab
name: square-pinterest
play-circle-o:
prefix: far
name: circle-play
plus-square-o:
prefix: far
name: square-plus
question-circle-o:
prefix: far
name: circle-question
ra:
prefix: fab
name: rebel
reddit-square:
prefix: fab
name: square-reddit
refresh:
name: arrows-rotate
registered:
prefix: far
remove:
name: xmark
reorder:
name: bars
repeat:
name: arrow-rotate-right
resistance:
prefix: fab
name: rebel
rmb:
name: yen-sign
rotate-left:
name: arrow-rotate-left
rotate-right:
name: arrow-rotate-right
rouble:
name: ruble-sign
rub:
name: ruble-sign
ruble:
name: ruble-sign
rupee:
name: indian-rupee-sign
s15:
name: bath
save:
prefix: far
name: floppy-disk
send:
name: paper-plane
send-o:
prefix: far
name: paper-plane
share-square-o:
name: share-from-square
shekel:
name: shekel-sign
sheqel:
name: shekel-sign
sign-in:
name: right-to-bracket
sign-out:
name: right-from-bracket
signing:
name: hands
smile-o:
prefix: far
name: face-smile
snapchat-ghost:
prefix: fab
name: snapchat
snapchat-square:
prefix: fab
name: square-snapchat
soccer-ball-o:
prefix: far
name: futbol
sort-alpha-asc:
name: arrow-down-a-z
sort-alpha-desc:
name: arrow-down-z-a
sort-amount-asc:
name: arrow-down-short-wide
sort-amount-desc:
name: arrow-down-wide-short
sort-asc:
name: sort-up
sort-desc:
name: sort-down
sort-numeric-asc:
name: arrow-down-1-9
sort-numeric-desc:
name: arrow-down-9-1
star-half-empty:
prefix: far
name: star-half-stroke
star-half-full:
prefix: far
name: star-half-stroke
star-half-o:
prefix: far
name: star-half-stroke
steam-square:
prefix: fab
name: square-steam
sticky-note-o:
prefix: far
name: note-sticky
stop-circle-o:
prefix: far
name: circle-stop
support:
name: life-ring
tablet:
name: tablet-screen-button
tachometer:
name: gauge-high
tasks:
name: bars-progress
television:
name: tv
thermometer:
name: temperature-full
thermometer-0:
name: temperature-empty
thermometer-1:
name: temperature-quarter
thermometer-2:
name: temperature-half
thermometer-3:
name: temperature-three-quarters
thermometer-4:
name: temperature-full
thumb-tack:
name: thumbtack
thumbs-o-down:
prefix: far
name: thumbs-down
thumbs-o-up:
prefix: far
name: thumbs-up
times-circle-o:
prefix: far
name: circle-xmark
times-rectangle:
name: rectangle-xmark
times-rectangle-o:
prefix: far
name: rectangle-xmark
toggle-down:
prefix: far
name: square-caret-down
toggle-left:
prefix: far
name: square-caret-left
toggle-right:
prefix: far
name: square-caret-right
toggle-up:
prefix: far
name: square-caret-up
transgender:
name: mars-and-venus
transgender-alt:
name: transgender
trash:
name: trash-can
trash-o:
prefix: far
name: trash-can
try:
name: turkish-lira-sign
tumblr-square:
prefix: fab
name: square-tumblr
turkish-lira:
name: turkish-lira-sign
twitter-square:
prefix: fab
name: square-twitter
unlink:
name: link-slash
unlock-alt:
name: unlock
unsorted:
name: sort
usd:
name: dollar-sign
user-circle-o:
prefix: far
name: circle-user
vcard:
name: address-card
vcard-o:
prefix: far
name: address-card
viadeo-square:
prefix: fab
name: square-viadeo
video-camera:
name: video
vimeo:
prefix: fab
name: vimeo-v
vimeo-square:
prefix: fab
name: square-vimeo
volume-control-phone:
name: phone-volume
warning:
name: triangle-exclamation
wechat:
prefix: fab
name: weixin
wheelchair-alt:
prefix: fab
name: accessible-icon
window-close-o:
prefix: far
name: rectangle-xmark
window-maximize:
prefix: far
window-restore:
prefix: far
won:
name: won-sign
xing-square:
prefix: fab
name: square-xing
y-combinator-square:
prefix: fab
name: hacker-news
yc:
prefix: fab
name: y-combinator
yc-square:
prefix: fab
name: hacker-news
yen:
name: yen-sign
youtube-play:
prefix: fab
name: youtube
youtube-square:
prefix: fab
name: square-youtube

File diff suppressed because it is too large Load Diff

1493
api/public/css/sprites/brands.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 500 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 116 KiB

4214
api/public/css/sprites/solid.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 876 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M320 96V416C341.011 416 361.818 411.861 381.23 403.821C400.641 395.78 418.28 383.995 433.138 369.138C447.995 354.28 459.78 336.641 467.821 317.23C475.861 297.818 480 277.011 480 256C480 234.989 475.861 214.182 467.821 194.771C459.78 175.359 447.995 157.72 433.138 142.863C418.28 128.005 400.641 116.22 381.23 108.179C361.818 100.139 341.011 96 320 96ZM0 256L160.002 416L320.003 256L160.002 96L0 256ZM480 256C480 277.011 484.138 297.818 492.179 317.23C500.219 336.643 512.005 354.28 526.862 369.138C541.72 383.995 559.357 395.781 578.77 403.821C598.182 411.862 618.989 416 640 416V96C597.565 96 556.869 112.858 526.862 142.863C496.857 172.869 480 213.565 480 256Z"/></svg>

After

Width:  |  Height:  |  Size: 953 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M103.3 344.3c-6.5-14.2-6.9-18.3 7.4-23.1 25.6-8 8 9.2 43.2 49.2h.3v-93.9c1.2-50.2 44-92.2 97.7-92.2 53.9 0 97.7 43.5 97.7 96.8 0 63.4-60.8 113.2-128.5 93.3-10.5-4.2-2.1-31.7 8.5-28.6 53 0 89.4-10.1 89.4-64.4 0-61-77.1-89.6-116.9-44.6-23.5 26.4-17.6 42.1-17.6 157.6 50.7 31 118.3 22 160.4-20.1 24.8-24.8 38.5-58 38.5-93 0-35.2-13.8-68.2-38.8-93.3-24.8-24.8-57.8-38.5-93.3-38.5s-68.8 13.8-93.5 38.5c-.3.3-16 16.5-21.2 23.9l-.5.6c-3.3 4.7-6.3 9.1-20.1 6.1-6.9-1.7-14.3-5.8-14.3-11.8V20c0-5 3.9-10.5 10.5-10.5h241.3c8.3 0 8.3 11.6 8.3 15.1 0 3.9 0 15.1-8.3 15.1H130.3v132.9h.3c104.2-109.8 282.8-36 282.8 108.9 0 178.1-244.8 220.3-310.1 62.8zm63.3-260.8c-.5 4.2 4.6 24.5 14.6 20.6C306 56.6 384 144.5 390.6 144.5c4.8 0 22.8-15.3 14.3-22.8-93.2-89-234.5-57-238.3-38.2zM393 414.7C283 524.6 94 475.5 61 310.5c0-12.2-30.4-7.4-28.9 3.3 24 173.4 246 256.9 381.6 121.3 6.9-7.8-12.6-28.4-20.7-20.4zM213.6 306.6c0 4 4.3 7.3 5.5 8.5 3 3 6.1 4.4 8.5 4.4 3.8 0 2.6.2 22.3-19.5 19.6 19.3 19.1 19.5 22.3 19.5 5.4 0 18.5-10.4 10.7-18.2L265.6 284l18.2-18.2c6.3-6.8-10.1-21.8-16.2-15.7L249.7 268c-18.6-18.8-18.4-19.5-21.5-19.5-5 0-18 11.7-12.4 17.3L234 284c-18.1 17.9-20.4 19.2-20.4 22.6z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M423.9 255.8L411 413.1c-3.3 40.7-63.9 35.1-60.6-4.9l10-122.5-41.1 2.3c10.1 20.7 15.8 43.9 15.8 68.5 0 41.2-16.1 78.7-42.3 106.5l-39.3-39.3c57.9-63.7 13.1-167.2-74-167.2-25.9 0-49.5 9.9-67.2 26L73 243.2c22-20.7 50.1-35.1 81.4-40.2l75.3-85.7-42.6-24.8-51.6 46c-30 26.8-70.6-18.5-40.5-45.4l68-60.7c9.8-8.8 24.1-10.2 35.5-3.6 0 0 139.3 80.9 139.5 81.1 16.2 10.1 20.7 36 6.1 52.6L285.7 229l106.1-5.9c18.5-1.1 33.6 14.4 32.1 32.7zm-64.9-154c28.1 0 50.9-22.8 50.9-50.9C409.9 22.8 387.1 0 359 0c-28.1 0-50.9 22.8-50.9 50.9 0 28.1 22.8 50.9 50.9 50.9zM179.6 456.5c-80.6 0-127.4-90.6-82.7-156.1l-39.7-39.7C36.4 287 24 320.3 24 356.4c0 130.7 150.7 201.4 251.4 122.5l-39.7-39.7c-16 10.9-35.3 17.3-56.1 17.3z"/></svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M322.1 252v-1l-51.2-65.8s-12 1.6-25 15.1c-9 9.3-242.1 239.1-243.4 240.9-7 10 1.6 6.8 15.7 1.7.8 0 114.5-36.6 114.5-36.6.5-.6-.1-.1.6-.6-.4-5.1-.8-26.2-1-27.7-.6-5.2 2.2-6.9 7-8.9l92.6-33.8c.6-.8 88.5-81.7 90.2-83.3zm160.1 120.1c13.3 16.1 20.7 13.3 30.8 9.3 3.2-1.2 115.4-47.6 117.8-48.9 8-4.3-1.7-16.7-7.2-23.4-2.1-2.5-205.1-245.6-207.2-248.3-9.7-12.2-14.3-12.9-38.4-12.8-10.2 0-106.8.5-116.5.6-19.2.1-32.9-.3-19.2 16.9C250 75 476.5 365.2 482.2 372.1zm152.7 1.6c-2.3-.3-24.6-4.7-38-7.2 0 0-115 50.4-117.5 51.6-16 7.3-26.9-3.2-36.7-14.6l-57.1-74c-5.4-.9-60.4-9.6-65.3-9.3-3.1.2-9.6.8-14.4 2.9-4.9 2.1-145.2 52.8-150.2 54.7-5.1 2-11.4 3.6-11.1 7.6.2 2.5 2 2.6 4.6 3.5 2.7.8 300.9 67.6 308 69.1 15.6 3.3 38.5 10.5 53.6 1.7 2.1-1.2 123.8-76.4 125.8-77.8 5.4-4 4.3-6.8-1.7-8.2z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M248 167.5l64.9 98.8H183.1l64.9-98.8zM496 256c0 136.9-111.1 248-248 248S0 392.9 0 256 111.1 8 248 8s248 111.1 248 248zm-99.8 82.7L248 115.5 99.8 338.7h30.4l33.6-51.7h168.6l33.6 51.7h30.2z"/></svg>

After

Width:  |  Height:  |  Size: 478 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M482.1 32H28.7C5.8 32 0 37.9 0 60.9v390.2C0 474.4 5.8 480 28.7 480h453.4c24.4 0 29.9-5.2 29.9-29.7V62.2c0-24.6-5.4-30.2-29.9-30.2zM178.4 220.3c-27.5-20.2-72.1-8.7-84.2 23.4-4.3 11.1-9.3 9.5-17.5 8.3-9.7-1.5-17.2-3.2-22.5-5.5-28.8-11.4 8.6-55.3 24.9-64.3 41.1-21.4 83.4-22.2 125.3-4.8 40.9 16.8 34.5 59.2 34.5 128.5 2.7 25.8-4.3 58.3 9.3 88.8 1.9 4.4.4 7.9-2.7 10.7-8.4 6.7-39.3 2.2-46.6-7.4-1.9-2.2-1.8-3.6-3.9-6.2-3.6-3.9-7.3-2.2-11.9 1-57.4 36.4-140.3 21.4-147-43.3-3.1-29.3 12.4-57.1 39.6-71 38.2-19.5 112.2-11.8 114-30.9 1.1-10.2-1.9-20.1-11.3-27.3zm286.7 222c0 15.1-11.1 9.9-17.8 9.9H52.4c-7.4 0-18.2 4.8-17.8-10.7.4-13.9 10.5-9.1 17.1-9.1 132.3-.4 264.5-.4 396.8 0 6.8 0 16.6-4.4 16.6 9.9zm3.8-340.5v291c0 5.7-.7 13.9-8.1 13.9-12.4-.4-27.5 7.1-36.1-5.6-5.8-8.7-7.8-4-12.4-1.2-53.4 29.7-128.1 7.1-144.4-85.2-6.1-33.4-.7-67.1 15.7-100 11.8-23.9 56.9-76.1 136.1-30.5v-71c0-26.2-.1-26.2 26-26.2 3.1 0 6.6.4 9.7 0 10.1-.8 13.6 4.4 13.6 14.3-.1.2-.1.3-.1.5zm-51.5 232.3c-19.5 47.6-72.9 43.3-90 5.2-15.1-33.3-15.5-68.2.4-101.5 16.3-34.1 59.7-35.7 81.5-4.8 20.6 28.8 14.9 84.6 8.1 101.1zm-294.8 35.3c-7.5-1.3-33-3.3-33.7-27.8-.4-13.9 7.8-23 19.8-25.8 24.4-5.9 49.3-9.9 73.7-14.7 8.9-2 7.4 4.4 7.8 9.5 1.4 33-26.1 59.2-67.6 58.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M159.7 237.4C108.4 308.3 43.1 348.2 14 326.6-15.2 304.9 2.8 230 54.2 159.1c51.3-70.9 116.6-110.8 145.7-89.2 29.1 21.6 11.1 96.6-40.2 167.5zm351.2-57.3C437.1 303.5 319 367.8 246.4 323.7c-25-15.2-41.3-41.2-49-73.8-33.6 64.8-92.8 113.8-164.1 133.2 49.8 59.3 124.1 96.9 207 96.9 150 0 271.6-123.1 271.6-274.9.1-8.5-.3-16.8-1-25z"/></svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M224 373.12c-25.24-31.67-40.08-59.43-45-83.18-22.55-88 112.61-88 90.06 0-5.45 24.25-20.29 52-45 83.18zm138.15 73.23c-42.06 18.31-83.67-10.88-119.3-50.47 103.9-130.07 46.11-200-18.85-200-54.92 0-85.16 46.51-73.28 100.5 6.93 29.19 25.23 62.39 54.43 99.5-32.53 36.05-60.55 52.69-85.15 54.92-50 7.43-89.11-41.06-71.3-91.09 15.1-39.16 111.72-231.18 115.87-241.56 15.75-30.07 25.56-57.4 59.38-57.4 32.34 0 43.4 25.94 60.37 59.87 36 70.62 89.35 177.48 114.84 239.09 13.17 33.07-1.37 71.29-37.01 86.64zm47-136.12C280.27 35.93 273.13 32 224 32c-45.52 0-64.87 31.67-84.66 72.79C33.18 317.1 22.89 347.19 22 349.81-3.22 419.14 48.74 480 111.63 480c21.71 0 60.61-6.06 112.37-62.4 58.68 63.78 101.26 62.4 112.37 62.4 62.89.05 114.85-60.86 89.61-130.19.02-3.89-16.82-38.9-16.82-39.58z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M256 0C116.1 0 2 112.7 0 252.1C-2 393.6 112.9 510.8 254.5 511.6c43.7 .3 85.9-10.4 123.3-30.7c3.6-2 4.2-7 1.1-9.7l-24-21.2c-4.9-4.3-11.8-5.5-17.8-3c-26.1 11.1-54.5 16.8-83.7 16.4C139 461.9 46.5 366.8 48.3 252.4C50.1 139.5 142.6 48.2 256 48.2H463.7V417.2L345.9 312.5c-3.8-3.4-9.7-2.7-12.7 1.3c-18.9 25-49.7 40.6-83.9 38.2c-47.5-3.3-85.9-41.5-89.5-88.9c-4.2-56.6 40.6-103.9 96.3-103.9c50.4 0 91.9 38.8 96.2 88c.4 4.4 2.4 8.5 5.7 11.4l30.7 27.2c3.5 3.1 9 1.2 9.9-3.4c2.2-11.8 3-24.2 2.1-36.8c-4.9-72-63.3-130-135.4-134.4c-82.7-5.1-151.8 59.5-154 140.6c-2.1 78.9 62.6 147 141.6 148.7c33 .7 63.6-9.6 88.3-27.6L495 509.4c6.6 5.8 17 1.2 17-7.7V9.7c0-5.4-4.4-9.7-9.7-9.7H256z"/></svg>

After

Width:  |  Height:  |  Size: 957 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M377.74 32H70.26C31.41 32 0 63.41 0 102.26v307.48C0 448.59 31.41 480 70.26 480h307.48c38.52 0 69.76-31.08 70.26-69.6-45.96-25.62-110.59-60.34-171.6-88.44-32.07 43.97-84.14 81-148.62 81-70.59 0-93.73-45.3-97.04-76.37-3.97-39.01 14.88-81.5 99.52-81.5 35.38 0 79.35 10.25 127.13 24.96 16.53-30.09 26.45-60.34 26.45-60.34h-178.2v-16.7h92.08v-31.24H88.28v-19.01h109.44V92.34h50.92v50.42h109.44v19.01H248.63v31.24h88.77s-15.21 46.62-38.35 90.92c48.93 16.7 100.01 36.04 148.62 52.74V102.26C447.83 63.57 416.43 32 377.74 32zM47.28 322.95c.99 20.17 10.25 53.73 69.93 53.73 52.07 0 92.58-39.68 117.87-72.9-44.63-18.68-84.48-31.41-109.44-31.41-67.45 0-79.35 33.06-78.36 50.58z"/></svg>

After

Width:  |  Height:  |  Size: 956 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M14 325.3c2.3-4.2 5.2-4.9 9.7-2.5 10.4 5.6 20.6 11.4 31.2 16.7a595.88 595.88 0 0 0 127.4 46.3 616.61 616.61 0 0 0 63.2 11.8 603.33 603.33 0 0 0 95 5.2c17.4-.4 34.8-1.8 52.1-3.8a603.66 603.66 0 0 0 163.3-42.8c2.9-1.2 5.9-2 9.1-1.2 6.7 1.8 9 9 4.1 13.9a70 70 0 0 1-9.6 7.4c-30.7 21.1-64.2 36.4-99.6 47.9a473.31 473.31 0 0 1-75.1 17.6 431 431 0 0 1-53.2 4.8 21.3 21.3 0 0 0-2.5.3H308a21.3 21.3 0 0 0-2.5-.3c-3.6-.2-7.2-.3-10.7-.4a426.3 426.3 0 0 1-50.4-5.3A448.4 448.4 0 0 1 164 420a443.33 443.33 0 0 1-145.6-87c-1.8-1.6-3-3.8-4.4-5.7zM172 65.1l-4.3.6a80.92 80.92 0 0 0-38 15.1c-2.4 1.7-4.6 3.5-7.1 5.4a4.29 4.29 0 0 1-.4-1.4c-.4-2.7-.8-5.5-1.3-8.2-.7-4.6-3-6.6-7.6-6.6h-11.5c-6.9 0-8.2 1.3-8.2 8.2v209.3c0 1 0 2 .1 3 .2 3 2 4.9 4.9 5 7 .1 14.1.1 21.1 0 2.9 0 4.7-2 5-5 .1-1 .1-2 .1-3v-72.4c1.1.9 1.7 1.4 2.2 1.9 17.9 14.9 38.5 19.8 61 15.4 20.4-4 34.6-16.5 43.8-34.9 7-13.9 9.9-28.7 10.3-44.1.5-17.1-1.2-33.9-8.1-49.8-8.5-19.6-22.6-32.5-43.9-36.9-3.2-.7-6.5-1-9.8-1.5-2.8-.1-5.5-.1-8.3-.1zM124.6 107a3.48 3.48 0 0 1 1.7-3.3c13.7-9.5 28.8-14.5 45.6-13.2 14.9 1.1 27.1 8.4 33.5 25.9 3.9 10.7 4.9 21.8 4.9 33 0 10.4-.8 20.6-4 30.6-6.8 21.3-22.4 29.4-42.6 28.5-14-.6-26.2-6-37.4-13.9a3.57 3.57 0 0 1-1.7-3.3c.1-14.1 0-28.1 0-42.2s.1-28 0-42.1zm205.7-41.9c-1 .1-2 .3-2.9.4a148 148 0 0 0-28.9 4.1c-6.1 1.6-12 3.8-17.9 5.8-3.6 1.2-5.4 3.8-5.3 7.7.1 3.3-.1 6.6 0 9.9.1 4.8 2.1 6.1 6.8 4.9 7.8-2 15.6-4.2 23.5-5.7 12.3-2.3 24.7-3.3 37.2-1.4 6.5 1 12.6 2.9 16.8 8.4 3.7 4.8 5.1 10.5 5.3 16.4.3 8.3.2 16.6.3 24.9a7.84 7.84 0 0 1-.2 1.4c-.5-.1-.9 0-1.3-.1a180.56 180.56 0 0 0-32-4.9c-11.3-.6-22.5.1-33.3 3.9-12.9 4.5-23.3 12.3-29.4 24.9-4.7 9.8-5.4 20.2-3.9 30.7 2 14 9 24.8 21.4 31.7 11.9 6.6 24.8 7.4 37.9 5.4 15.1-2.3 28.5-8.7 40.3-18.4a7.36 7.36 0 0 1 1.6-1.1c.6 3.8 1.1 7.4 1.8 11 .6 3.1 2.5 5.1 5.4 5.2 5.4.1 10.9.1 16.3 0a4.84 4.84 0 0 0 4.8-4.7 26.2 26.2 0 0 0 .1-2.8v-106a80 80 0 0 0-.9-12.9c-1.9-12.9-7.4-23.5-19-30.4-6.7-4-14.1-6-21.8-7.1-3.6-.5-7.2-.8-10.8-1.3-3.9.1-7.9.1-11.9.1zm35 127.7a3.33 3.33 0 0 1-1.5 3c-11.2 8.1-23.5 13.5-37.4 14.9-5.7.6-11.4.4-16.8-1.8a20.08 20.08 0 0 1-12.4-13.3 32.9 32.9 0 0 1-.1-19.4c2.5-8.3 8.4-13 16.4-15.6a61.33 61.33 0 0 1 24.8-2.2c8.4.7 16.6 2.3 25 3.4 1.6.2 2.1 1 2.1 2.6-.1 4.8 0 9.5 0 14.3s-.2 9.4-.1 14.1zm259.9 129.4c-1-5-4.8-6.9-9.1-8.3a88.42 88.42 0 0 0-21-3.9 147.32 147.32 0 0 0-39.2 1.9c-14.3 2.7-27.9 7.3-40 15.6a13.75 13.75 0 0 0-3.7 3.5 5.11 5.11 0 0 0-.5 4c.4 1.5 2.1 1.9 3.6 1.8a16.2 16.2 0 0 0 2.2-.1c7.8-.8 15.5-1.7 23.3-2.5 11.4-1.1 22.9-1.8 34.3-.9a71.64 71.64 0 0 1 14.4 2.7c5.1 1.4 7.4 5.2 7.6 10.4.4 8-1.4 15.7-3.5 23.3-4.1 15.4-10 30.3-15.8 45.1a17.6 17.6 0 0 0-1 3c-.5 2.9 1.2 4.8 4.1 4.1a10.56 10.56 0 0 0 4.8-2.5 145.91 145.91 0 0 0 12.7-13.4c12.8-16.4 20.3-35.3 24.7-55.6.8-3.6 1.4-7.3 2.1-10.9v-17.3zM493.1 199q-19.35-53.55-38.7-107.2c-2-5.7-4.2-11.3-6.3-16.9-1.1-2.9-3.2-4.8-6.4-4.8-7.6-.1-15.2-.2-22.9-.1-2.5 0-3.7 2-3.2 4.5a43.1 43.1 0 0 0 1.9 6.1q29.4 72.75 59.1 145.5c1.7 4.1 2.1 7.6.2 11.8-3.3 7.3-5.9 15-9.3 22.3-3 6.5-8 11.4-15.2 13.3a42.13 42.13 0 0 1-15.4 1.1c-2.5-.2-5-.8-7.5-1-3.4-.2-5.1 1.3-5.2 4.8q-.15 5 0 9.9c.1 5.5 2 8 7.4 8.9a108.18 108.18 0 0 0 16.9 2c17.1.4 30.7-6.5 39.5-21.4a131.63 131.63 0 0 0 9.2-18.4q35.55-89.7 70.6-179.6a26.62 26.62 0 0 0 1.6-5.5c.4-2.8-.9-4.4-3.7-4.4-6.6-.1-13.3 0-19.9 0a7.54 7.54 0 0 0-7.7 5.2c-.5 1.4-1.1 2.7-1.6 4.1l-34.8 100c-2.5 7.2-5.1 14.5-7.7 22.2-.4-1.1-.6-1.7-.9-2.4z"/></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M257.2 162.7c-48.7 1.8-169.5 15.5-169.5 117.5 0 109.5 138.3 114 183.5 43.2 6.5 10.2 35.4 37.5 45.3 46.8l56.8-56S341 288.9 341 261.4V114.3C341 89 316.5 32 228.7 32 140.7 32 94 87 94 136.3l73.5 6.8c16.3-49.5 54.2-49.5 54.2-49.5 40.7-.1 35.5 29.8 35.5 69.1zm0 86.8c0 80-84.2 68-84.2 17.2 0-47.2 50.5-56.7 84.2-57.8v40.6zm136 163.5c-7.7 10-70 67-174.5 67S34.2 408.5 9.7 379c-6.8-7.7 1-11.3 5.5-8.3C88.5 415.2 203 488.5 387.7 401c7.5-3.7 13.3 2 5.5 12zm39.8 2.2c-6.5 15.8-16 26.8-21.2 31-5.5 4.5-9.5 2.7-6.5-3.8s19.3-46.5 12.7-55c-6.5-8.3-37-4.3-48-3.2-10.8 1-13 2-14-.3-2.3-5.7 21.7-15.5 37.5-17.5 15.7-1.8 41-.8 46 5.7 3.7 5.1 0 27.1-6.5 43.1z"/></svg>

After

Width:  |  Height:  |  Size: 931 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M240.1 32c-61.9 0-131.5 16.9-184.2 55.4-5.1 3.1-9.1 9.2-7.2 19.4 1.1 5.1 5.1 27.4 10.2 39.6 4.1 10.2 14.2 10.2 20.3 6.1 32.5-22.3 96.5-47.7 152.3-47.7 57.9 0 58.9 28.4 58.9 73.1v38.5C203 227.7 78.2 251 46.7 264.2 11.2 280.5 16.3 357.7 16.3 376s15.2 104 124.9 104c47.8 0 113.7-20.7 153.3-42.1v25.4c0 3 2.1 8.2 6.1 9.1 3.1 1 50.7 2 59.9 2s62.5.3 66.5-.7c4.1-1 5.1-6.1 5.1-9.1V168c-.1-80.3-57.9-136-192-136zm50.2 348c-21.4 13.2-48.7 24.4-79.1 24.4-52.8 0-58.9-33.5-59-44.7 0-12.2-3-42.7 18.3-52.9 24.3-13.2 75.1-29.4 119.8-33.5z"/></svg>

After

Width:  |  Height:  |  Size: 816 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M420.55,301.93a24,24,0,1,1,24-24,24,24,0,0,1-24,24m-265.1,0a24,24,0,1,1,24-24,24,24,0,0,1-24,24m273.7-144.48,47.94-83a10,10,0,1,0-17.27-10h0l-48.54,84.07a301.25,301.25,0,0,0-246.56,0L116.18,64.45a10,10,0,1,0-17.27,10h0l47.94,83C64.53,202.22,8.24,285.55,0,384H576c-8.24-98.45-64.54-181.78-146.85-226.55"/></svg>

After

Width:  |  Height:  |  Size: 592 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M347.1 215.4c11.7-32.6 45.4-126.9 45.4-157.1 0-26.6-15.7-48.9-43.7-48.9-44.6 0-84.6 131.7-97.1 163.1C242 144 196.6 0 156.6 0c-31.1 0-45.7 22.9-45.7 51.7 0 35.3 34.2 126.8 46.6 162-6.3-2.3-13.1-4.3-20-4.3-23.4 0-48.3 29.1-48.3 52.6 0 8.9 4.9 21.4 8 29.7-36.9 10-51.1 34.6-51.1 71.7C46 435.6 114.4 512 210.6 512c118 0 191.4-88.6 191.4-202.9 0-43.1-6.9-82-54.9-93.7zM311.7 108c4-12.3 21.1-64.3 37.1-64.3 8.6 0 10.9 8.9 10.9 16 0 19.1-38.6 124.6-47.1 148l-34-6 33.1-93.7zM142.3 48.3c0-11.9 14.5-45.7 46.3 47.1l34.6 100.3c-15.6-1.3-27.7-3-35.4 1.4-10.9-28.8-45.5-119.7-45.5-148.8zM140 244c29.3 0 67.1 94.6 67.1 107.4 0 5.1-4.9 11.4-10.6 11.4-20.9 0-76.9-76.9-76.9-97.7.1-7.7 12.7-21.1 20.4-21.1zm184.3 186.3c-29.1 32-66.3 48.6-109.7 48.6-59.4 0-106.3-32.6-128.9-88.3-17.1-43.4 3.8-68.3 20.6-68.3 11.4 0 54.3 60.3 54.3 73.1 0 4.9-7.7 8.3-11.7 8.3-16.1 0-22.4-15.5-51.1-51.4-29.7 29.7 20.5 86.9 58.3 86.9 26.1 0 43.1-24.2 38-42 3.7 0 8.3.3 11.7-.6 1.1 27.1 9.1 59.4 41.7 61.7 0-.9 2-7.1 2-7.4 0-17.4-10.6-32.6-10.6-50.3 0-28.3 21.7-55.7 43.7-71.7 8-6 17.7-9.7 27.1-13.1 9.7-3.7 20-8 27.4-15.4-1.1-11.2-5.7-21.1-16.9-21.1-27.7 0-120.6 4-120.6-39.7 0-6.7.1-13.1 17.4-13.1 32.3 0 114.3 8 138.3 29.1 18.1 16.1 24.3 113.2-31 174.7zm-98.6-126c9.7 3.1 19.7 4 29.7 6-7.4 5.4-14 12-20.3 19.1-2.8-8.5-6.2-16.8-9.4-25.1z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M640 238.2l-3.2 28.2-34.5 2.3-2 18.1 34.5-2.3-3.2 28.2-34.4 2.2-2.3 20.1 34.4-2.2-3 26.1-64.7 4.1 12.7-113.2L527 365.2l-31.9 2-23.8-117.8 30.3-2 13.6 79.4 31.7-82.4 93.1-6.2zM426.8 371.5l28.3-1.8L468 249.6l-28.4 1.9-12.8 120zM162 388.1l-19.4-36-3.5 37.4-28.2 1.7 2.7-29.1c-11 18-32 34.3-56.9 35.8C23.9 399.9-3 377 .3 339.7c2.6-29.3 26.7-62.8 67.5-65.4 37.7-2.4 47.6 23.2 51.3 28.8l2.8-30.8 38.9-2.5c20.1-1.3 38.7 3.7 42.5 23.7l2.6-26.6 64.8-4.2-2.7 27.9-36.4 2.4-1.7 17.9 36.4-2.3-2.7 27.9-36.4 2.3-1.9 19.9 36.3-2.3-2.1 20.8 55-117.2 23.8-1.6L370.4 369l8.9-85.6-22.3 1.4 2.9-27.9 75-4.9-3 28-24.3 1.6-9.7 91.9-58 3.7-4.3-15.6-39.4 2.5-8 16.3-126.2 7.7zm-44.3-70.2l-26.4 1.7C84.6 307.2 76.9 303 65 303.8c-19 1.2-33.3 17.5-34.6 33.3-1.4 16 7.3 32.5 28.7 31.2 12.8-.8 21.3-8.6 28.9-18.9l27-1.7 2.7-29.8zm56.1-7.7c1.2-12.9-7.6-13.6-26.1-12.4l-2.7 28.5c14.2-.9 27.5-2.1 28.8-16.1zm21.1 70.8l5.8-60c-5 13.5-14.7 21.1-27.9 26.6l22.1 33.4zm135.4-45l-7.9-37.8-15.8 39.3 23.7-1.5zm-170.1-74.6l-4.3-17.5-39.6 2.6-8.1 18.2-31.9 2.1 57-121.9 23.9-1.6 30.7 102 9.9-104.7 27-1.8 37.8 63.6 6.5-66.6 28.5-1.9-4 41.2c7.4-13.5 22.9-44.7 63.6-47.5 40.5-2.8 52.4 29.3 53.4 30.3l3.3-32 39.3-2.7c12.7-.9 27.8.3 36.3 9.7l-4.4-11.9 32.2-2.2 12.9 43.2 23-45.7 31-2.2-43.6 78.4-4.8 44.3-28.4 1.9 4.8-44.3-15.8-43c1 22.3-9.2 40.1-32 49.6l25.2 38.8-36.4 2.4-19.2-36.8-4 38.3-28.4 1.9 3.3-31.5c-6.7 9.3-19.7 35.4-59.6 38-26.2 1.7-45.6-10.3-55.4-39.2l-4 40.3-25 1.6-37.6-63.3-6.3 66.2-56.8 3.7zm276.6-82.1c10.2-.7 17.5-2.1 21.6-4.3 4.5-2.4 7-6.4 7.6-12.1.6-5.3-.6-8.8-3.4-10.4-3.6-2.1-10.6-2.8-22.9-2l-2.9 28.8zM327.7 214c5.6 5.9 12.7 8.5 21.3 7.9 4.7-.3 9.1-1.8 13.3-4.1 5.5-3 10.6-8 15.1-14.3l-34.2 2.3 2.4-23.9 63.1-4.3 1.2-12-31.2 2.1c-4.1-3.7-7.8-6.6-11.1-8.1-4-1.7-8.1-2.8-12.2-2.5-8 .5-15.3 3.6-22 9.2-7.7 6.4-12 14.5-12.9 24.4-1.1 9.6 1.4 17.3 7.2 23.3zm-201.3 8.2l23.8-1.6-8.3-37.6-15.5 39.2z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M185.7 268.1h76.2l-38.1-91.6-38.1 91.6zM223.8 32L16 106.4l31.8 275.7 176 97.9 176-97.9 31.8-275.7zM354 373.8h-48.6l-26.2-65.4H168.6l-26.2 65.4H93.7L223.8 81.5z"/></svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM127 384.5c-5.5 9.6-17.8 12.8-27.3 7.3-9.6-5.5-12.8-17.8-7.3-27.3l14.3-24.7c16.1-4.9 29.3-1.1 39.6 11.4L127 384.5zm138.9-53.9H84c-11 0-20-9-20-20s9-20 20-20h51l65.4-113.2-20.5-35.4c-5.5-9.6-2.2-21.8 7.3-27.3 9.6-5.5 21.8-2.2 27.3 7.3l8.9 15.4 8.9-15.4c5.5-9.6 17.8-12.8 27.3-7.3 9.6 5.5 12.8 17.8 7.3 27.3l-85.8 148.6h62.1c20.2 0 31.5 23.7 22.7 40zm98.1 0h-29l19.6 33.9c5.5 9.6 2.2 21.8-7.3 27.3-9.6 5.5-21.8 2.2-27.3-7.3-32.9-56.9-57.5-99.7-74-128.1-16.7-29-4.8-58 7.1-67.8 13.1 22.7 32.7 56.7 58.9 102h52c11 0 20 9 20 20 0 11.1-9 20-20 20z"/></svg>

After

Width:  |  Height:  |  Size: 937 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M255.9 120.9l9.1-15.7c5.6-9.8 18.1-13.1 27.9-7.5 9.8 5.6 13.1 18.1 7.5 27.9l-87.5 151.5h63.3c20.5 0 32 24.1 23.1 40.8H113.8c-11.3 0-20.4-9.1-20.4-20.4 0-11.3 9.1-20.4 20.4-20.4h52l66.6-115.4-20.8-36.1c-5.6-9.8-2.3-22.2 7.5-27.9 9.8-5.6 22.2-2.3 27.9 7.5l8.9 15.7zm-78.7 218l-19.6 34c-5.6 9.8-18.1 13.1-27.9 7.5-9.8-5.6-13.1-18.1-7.5-27.9l14.6-25.2c16.4-5.1 29.8-1.2 40.4 11.6zm168.9-61.7h53.1c11.3 0 20.4 9.1 20.4 20.4 0 11.3-9.1 20.4-20.4 20.4h-29.5l19.9 34.5c5.6 9.8 2.3 22.2-7.5 27.9-9.8 5.6-22.2 2.3-27.9-7.5-33.5-58.1-58.7-101.6-75.4-130.6-17.1-29.5-4.9-59.1 7.2-69.1 13.4 23 33.4 57.7 60.1 104zM256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm216 248c0 118.7-96.1 216-216 216-118.7 0-216-96.1-216-216 0-118.7 96.1-216 216-216 118.7 0 216 96.1 216 216z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M42.1 239.1c22.2 0 29 2.8 33.5 14.6h.8v-22.9c0-11.3-4.8-15.4-17.9-15.4-11.3 0-14.4 2.5-15.1 12.8H4.8c.3-13.9 1.5-19.1 5.8-24.4C17.9 195 29.5 192 56.7 192c33 0 47.1 5 53.9 18.9 2 4.3 4 15.6 4 23.7v76.3H76.3l1.3-19.1h-1c-5.3 15.6-13.6 20.4-35.5 20.4-30.3 0-41.1-10.1-41.1-37.3 0-25.2 12.3-35.8 42.1-35.8zm17.1 48.1c13.1 0 16.9-3 16.9-13.4 0-9.1-4.3-11.6-19.6-11.6-13.1 0-17.9 3-17.9 12.1-.1 10.4 3.7 12.9 20.6 12.9zm77.8-94.9h38.3l-1.5 20.6h.8c9.1-17.1 15.9-20.9 37.5-20.9 14.4 0 24.7 3 31.5 9.1 9.8 8.6 12.8 20.4 12.8 48.1 0 30-3 43.1-12.1 52.9-6.8 7.3-16.4 10.1-33.2 10.1-20.4 0-29.2-5.5-33.8-21.2h-.8v70.3H137v-169zm80.9 60.7c0-27.5-3.3-32.5-20.7-32.5-16.9 0-20.7 5-20.7 28.7 0 28 3.5 33.5 21.2 33.5 16.4 0 20.2-5.6 20.2-29.7zm57.9-60.7h38.3l-1.5 20.6h.8c9.1-17.1 15.9-20.9 37.5-20.9 14.4 0 24.7 3 31.5 9.1 9.8 8.6 12.8 20.4 12.8 48.1 0 30-3 43.1-12.1 52.9-6.8 7.3-16.4 10.1-33.3 10.1-20.4 0-29.2-5.5-33.8-21.2h-.8v70.3h-39.5v-169zm80.9 60.7c0-27.5-3.3-32.5-20.7-32.5-16.9 0-20.7 5-20.7 28.7 0 28 3.5 33.5 21.2 33.5 16.4 0 20.2-5.6 20.2-29.7zm53.8-3.8c0-25.4 3.3-37.8 12.3-45.8 8.8-8.1 22.2-11.3 45.1-11.3 42.8 0 55.7 12.8 55.7 55.7v11.1h-75.3c-.3 2-.3 4-.3 4.8 0 16.9 4.5 21.9 20.1 21.9 13.9 0 17.9-3 17.9-13.9h37.5v2.3c0 9.8-2.5 18.9-6.8 24.7-7.3 9.8-19.6 13.6-44.3 13.6-27.5 0-41.6-3.3-50.6-12.3-8.5-8.5-11.3-21.3-11.3-50.8zm76.4-11.6c-.3-1.8-.3-3.3-.3-3.8 0-12.3-3.3-14.6-19.6-14.6-14.4 0-17.1 3-18.1 15.1l-.3 3.3h38.3zm55.6-45.3h38.3l-1.8 19.9h.7c6.8-14.9 14.4-20.2 29.7-20.2 10.8 0 19.1 3.3 23.4 9.3 5.3 7.3 6.8 14.4 6.8 34 0 1.5 0 5 .2 9.3h-35c.3-1.8.3-3.3.3-4 0-15.4-2-19.4-10.3-19.4-6.3 0-10.8 3.3-13.1 9.3-1 3-1 4.3-1 12.3v68h-38.3V192.3z"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M116.9 158.5c-7.5 8.9-19.5 15.9-31.5 14.9-1.5-12 4.4-24.8 11.3-32.6 7.5-9.1 20.6-15.6 31.3-16.1 1.2 12.4-3.7 24.7-11.1 33.8m10.9 17.2c-17.4-1-32.3 9.9-40.5 9.9-8.4 0-21-9.4-34.8-9.1-17.9.3-34.5 10.4-43.6 26.5-18.8 32.3-4.9 80 13.3 106.3 8.9 13 19.5 27.3 33.5 26.8 13.3-.5 18.5-8.6 34.5-8.6 16.1 0 20.8 8.6 34.8 8.4 14.5-.3 23.6-13 32.5-26 10.1-14.8 14.3-29.1 14.5-29.9-.3-.3-28-10.9-28.3-42.9-.3-26.8 21.9-39.5 22.9-40.3-12.5-18.6-32-20.6-38.8-21.1m100.4-36.2v194.9h30.3v-66.6h41.9c38.3 0 65.1-26.3 65.1-64.3s-26.4-64-64.1-64h-73.2zm30.3 25.5h34.9c26.3 0 41.3 14 41.3 38.6s-15 38.8-41.4 38.8h-34.8V165zm162.2 170.9c19 0 36.6-9.6 44.6-24.9h.6v23.4h28v-97c0-28.1-22.5-46.3-57.1-46.3-32.1 0-55.9 18.4-56.8 43.6h27.3c2.3-12 13.4-19.9 28.6-19.9 18.5 0 28.9 8.6 28.9 24.5v10.8l-37.8 2.3c-35.1 2.1-54.1 16.5-54.1 41.5.1 25.2 19.7 42 47.8 42zm8.2-23.1c-16.1 0-26.4-7.8-26.4-19.6 0-12.3 9.9-19.4 28.8-20.5l33.6-2.1v11c0 18.2-15.5 31.2-36 31.2zm102.5 74.6c29.5 0 43.4-11.3 55.5-45.4L640 193h-30.8l-35.6 115.1h-.6L537.4 193h-31.6L557 334.9l-2.8 8.6c-4.6 14.6-12.1 20.3-25.5 20.3-2.4 0-7-.3-8.9-.5v23.4c1.8.4 9.3.7 11.6.7z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg>

After

Width:  |  Height:  |  Size: 726 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M2 377.4l43 74.3A51.35 51.35 0 0 0 90.9 480h285.4l-59.2-102.6zM501.8 350L335.6 59.3A51.38 51.38 0 0 0 290.2 32h-88.4l257.3 447.6 40.7-70.5c1.9-3.2 21-29.7 2-59.1zM275 304.5l-115.5-200L44 304.5z"/></svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M517.5 309.2c38.8-40 58.1-80 58.5-116.1.8-65.5-59.4-118.2-169.4-135C277.9 38.4 118.1 73.6 0 140.5 52 114 110.6 92.3 170.7 82.3c74.5-20.5 153-25.4 221.3-14.8C544.5 91.3 588.8 195 490.8 299.2c-10.2 10.8-22 21.1-35 30.6L304.9 103.4 114.7 388.9c-65.6-29.4-76.5-90.2-19.1-151.2 20.8-22.2 48.3-41.9 79.5-58.1 20-12.2 39.7-22.6 62-30.7-65.1 20.3-122.7 52.9-161.6 92.9-27.7 28.6-41.4 57.1-41.7 82.9-.5 35.1 23.4 65.1 68.4 83l-34.5 51.7h101.6l22-34.4c22.2 1 45.3 0 68.6-2.7l-22.8 37.1h135.5L340 406.3c18.6-5.3 36.9-11.5 54.5-18.7l45.9 71.8H542L468.6 349c18.5-12.1 35-25.5 48.9-39.8zm-187.6 80.5l-25-40.6-32.7 53.3c-23.4 3.5-46.7 5.1-69.2 4.4l101.9-159.3 78.7 123c-17.2 7.4-35.3 13.9-53.7 19.2z"/></svg>

After

Width:  |  Height:  |  Size: 975 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M152.2 236.4c-7.7-8.2-19.7-7.7-24.8 2.8L1.6 490.2c-5 10 2.4 21.7 13.4 21.7h175c5.8.1 11-3.2 13.4-8.4 37.9-77.8 15.1-196.3-51.2-267.1zM244.4 8.1c-122.3 193.4-8.5 348.6 65 495.5 2.5 5.1 7.7 8.4 13.4 8.4H497c11.2 0 18.4-11.8 13.4-21.7 0 0-234.5-470.6-240.4-482.3-5.3-10.6-18.8-10.8-25.6.1z"/></svg>

After

Width:  |  Height:  |  Size: 577 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M640 199.9v54l-320 200L0 254v-54l320 200 320-200.1zm-194.5 72l47.1-29.4c-37.2-55.8-100.7-92.6-172.7-92.6-72 0-135.5 36.7-172.6 92.4h.3c2.5-2.3 5.1-4.5 7.7-6.7 89.7-74.4 219.4-58.1 290.2 36.3zm-220.1 18.8c16.9-11.9 36.5-18.7 57.4-18.7 34.4 0 65.2 18.4 86.4 47.6l45.4-28.4c-20.9-29.9-55.6-49.5-94.8-49.5-38.9 0-73.4 19.4-94.4 49zM103.6 161.1c131.8-104.3 318.2-76.4 417.5 62.1l.7 1 48.8-30.4C517.1 112.1 424.8 58.1 319.9 58.1c-103.5 0-196.6 53.5-250.5 135.6 9.9-10.5 22.7-23.5 34.2-32.6zm467 32.7z"/></svg>

After

Width:  |  Height:  |  Size: 785 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M318.4 16l-161 480h77.5l25.4-81.4h119.5L405 496h77.5L318.4 16zm-40.3 341.9l41.2-130.4h1.5l40.9 130.4h-83.6zM640 405l-10-31.4L462.1 358l19.4 56.5L640 405zm-462.1-47L10 373.7 0 405l158.5 9.4 19.4-56.4z"/></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M453.1 32h-312c-38.9 0-76.2 31.2-83.3 69.7L1.2 410.3C-5.9 448.8 19.9 480 58.9 480h312c38.9 0 76.2-31.2 83.3-69.7l56.7-308.5c7-38.6-18.8-69.8-57.8-69.8zm-58.2 347.3l-32 13.5-115.4-110c-14.7 10-29.2 19.5-41.7 27.1l22.1 64.2-17.9 12.7-40.6-61-52.4-48.1 15.7-15.4 58 31.1c9.3-10.5 20.8-22.6 32.8-34.9L203 228.9l-68.8-99.8 18.8-28.9 8.9-4.8L265 207.8l4.9 4.5c19.4-18.8 33.8-32.4 33.8-32.4 7.7-6.5 21.5-2.9 30.7 7.9 9 10.5 10.6 24.7 2.7 31.3-1.8 1.3-15.5 11.4-35.3 25.6l4.5 7.3 94.9 119.4-6.3 7.9z"/></svg>

After

Width:  |  Height:  |  Size: 782 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M107.2 283.5l-19-41.8H36.1l-19 41.8H0l62.2-131.4 62.2 131.4h-17.2zm-45-98.1l-19.6 42.5h39.2l-19.6-42.5zm112.7 102.4l-62.2-131.4h17.1l45.1 96 45.1-96h17l-62.1 131.4zm80.6-4.3V156.4H271v127.1h-15.5zm209.1-115.6v115.6h-17.3V167.9h-41.2v-11.5h99.6v11.5h-41.1zM640 218.8c0 9.2-1.7 17.8-5.1 25.8-3.4 8-8.2 15.1-14.2 21.1-6 6-13.1 10.8-21.1 14.2-8 3.4-16.6 5.1-25.8 5.1s-17.8-1.7-25.8-5.1c-8-3.4-15.1-8.2-21.1-14.2-6-6-10.8-13-14.2-21.1-3.4-8-5.1-16.6-5.1-25.8s1.7-17.8 5.1-25.8c3.4-8 8.2-15.1 14.2-21.1 6-6 13-8.4 21.1-11.9 8-3.4 16.6-5.1 25.8-5.1s17.8 1.7 25.8 5.1c8 3.4 15.1 5.8 21.1 11.9 6 6 10.7 13.1 14.2 21.1 3.4 8 5.1 16.6 5.1 25.8zm-15.5 0c0-7.3-1.3-14-3.9-20.3-2.6-6.3-6.2-11.7-10.8-16.3-4.6-4.6-10-8.2-16.2-10.9-6.2-2.7-12.8-4-19.8-4s-13.6 1.3-19.8 4c-6.2 2.7-11.6 6.3-16.2 10.9-4.6 4.6-8.2 10-10.8 16.3-2.6 6.3-3.9 13.1-3.9 20.3 0 7.3 1.3 14 3.9 20.3 2.6 6.3 6.2 11.7 10.8 16.3 4.6 4.6 10 8.2 16.2 10.9 6.2 2.7 12.8 4 19.8 4s13.6-1.3 19.8-4c6.2-2.7 11.6-6.3 16.2-10.9 4.6-4.6 8.2-10 10.8-16.3 2.6-6.3 3.9-13.1 3.9-20.3zm-94.8 96.7v-6.3l88.9-10-242.9 13.4c.6-2.2 1.1-4.6 1.4-7.2.3-2 .5-4.2.6-6.5l64.8-8.1-64.9 1.9c0-.4-.1-.7-.1-1.1-2.8-17.2-25.5-23.7-25.5-23.7l-1.1-26.3h23.8l19 41.8h17.1L348.6 152l-62.2 131.4h17.1l19-41.8h23.6L345 268s-22.7 6.5-25.5 23.7c-.1.3-.1.7-.1 1.1l-64.9-1.9 64.8 8.1c.1 2.3.3 4.4.6 6.5.3 2.6.8 5 1.4 7.2L78.4 299.2l88.9 10v6.3c-5.9.9-10.5 6-10.5 12.2 0 6.8 5.6 12.4 12.4 12.4 6.8 0 12.4-5.6 12.4-12.4 0-6.2-4.6-11.3-10.5-12.2v-5.8l80.3 9v5.4c-5.7 1.1-9.9 6.2-9.9 12.1 0 6.8 5.6 10.2 12.4 10.2 6.8 0 12.4-3.4 12.4-10.2 0-6-4.3-11-9.9-12.1v-4.9l28.4 3.2v23.7h-5.9V360h5.9v-6.6h5v6.6h5.9v-13.8h-5.9V323l38.3 4.3c8.1 11.4 19 13.6 19 13.6l-.1 6.7-5.1.2-.1 12.1h4.1l.1-5h5.2l.1 5h4.1l-.1-12.1-5.1-.2-.1-6.7s10.9-2.1 19-13.6l38.3-4.3v23.2h-5.9V360h5.9v-6.6h5v6.6h5.9v-13.8h-5.9v-23.7l28.4-3.2v4.9c-5.7 1.1-9.9 6.2-9.9 12.1 0 6.8 5.6 10.2 12.4 10.2 6.8 0 12.4-3.4 12.4-10.2 0-6-4.3-11-9.9-12.1v-5.4l80.3-9v5.8c-5.9.9-10.5 6-10.5 12.2 0 6.8 5.6 12.4 12.4 12.4 6.8 0 12.4-5.6 12.4-12.4-.2-6.3-4.7-11.4-10.7-12.3zm-200.8-87.6l19.6-42.5 19.6 42.5h-17.9l-1.7-40.3-1.7 40.3h-17.9z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M180.41 203.01c-.72 22.65 10.6 32.68 10.88 39.05a8.164 8.164 0 0 1-4.1 6.27l-12.8 8.96a10.66 10.66 0 0 1-5.63 1.92c-.43-.02-8.19 1.83-20.48-25.61a78.608 78.608 0 0 1-62.61 29.45c-16.28.89-60.4-9.24-58.13-56.21-1.59-38.28 34.06-62.06 70.93-60.05 7.1.02 21.6.37 46.99 6.27v-15.62c2.69-26.46-14.7-46.99-44.81-43.91-2.4.01-19.4-.5-45.84 10.11-7.36 3.38-8.3 2.82-10.75 2.82-7.41 0-4.36-21.48-2.94-24.2 5.21-6.4 35.86-18.35 65.94-18.18a76.857 76.857 0 0 1 55.69 17.28 70.285 70.285 0 0 1 17.67 52.36l-.01 69.29zM93.99 235.4c32.43-.47 46.16-19.97 49.29-30.47 2.46-10.05 2.05-16.41 2.05-27.4-9.67-2.32-23.59-4.85-39.56-4.87-15.15-1.14-42.82 5.63-41.74 32.26-1.24 16.79 11.12 31.4 29.96 30.48zm170.92 23.05c-7.86.72-11.52-4.86-12.68-10.37l-49.8-164.65c-.97-2.78-1.61-5.65-1.92-8.58a4.61 4.61 0 0 1 3.86-5.25c.24-.04-2.13 0 22.25 0 8.78-.88 11.64 6.03 12.55 10.37l35.72 140.83 33.16-140.83c.53-3.22 2.94-11.07 12.8-10.24h17.16c2.17-.18 11.11-.5 12.68 10.37l33.42 142.63L420.98 80.1c.48-2.18 2.72-11.37 12.68-10.37h19.72c.85-.13 6.15-.81 5.25 8.58-.43 1.85 3.41-10.66-52.75 169.9-1.15 5.51-4.82 11.09-12.68 10.37h-18.69c-10.94 1.15-12.51-9.66-12.68-10.75L328.67 110.7l-32.78 136.99c-.16 1.09-1.73 11.9-12.68 10.75h-18.3zm273.48 5.63c-5.88.01-33.92-.3-57.36-12.29a12.802 12.802 0 0 1-7.81-11.91v-10.75c0-8.45 6.2-6.9 8.83-5.89 10.04 4.06 16.48 7.14 28.81 9.6 36.65 7.53 52.77-2.3 56.72-4.48 13.15-7.81 14.19-25.68 5.25-34.95-10.48-8.79-15.48-9.12-53.13-21-4.64-1.29-43.7-13.61-43.79-52.36-.61-28.24 25.05-56.18 69.52-55.95 12.67-.01 46.43 4.13 55.57 15.62 1.35 2.09 2.02 4.55 1.92 7.04v10.11c0 4.44-1.62 6.66-4.87 6.66-7.71-.86-21.39-11.17-49.16-10.75-6.89-.36-39.89.91-38.41 24.97-.43 18.96 26.61 26.07 29.7 26.89 36.46 10.97 48.65 12.79 63.12 29.58 17.14 22.25 7.9 48.3 4.35 55.44-19.08 37.49-68.42 34.44-69.26 34.42zm40.2 104.86c-70.03 51.72-171.69 79.25-258.49 79.25A469.127 469.127 0 0 1 2.83 327.46c-6.53-5.89-.77-13.96 7.17-9.47a637.37 637.37 0 0 0 316.88 84.12 630.22 630.22 0 0 0 241.59-49.55c11.78-5 21.77 7.8 10.12 16.38zm29.19-33.29c-8.96-11.52-59.28-5.38-81.81-2.69-6.79.77-7.94-5.12-1.79-9.47 40.07-28.17 105.88-20.1 113.44-10.63 7.55 9.47-2.05 75.41-39.56 106.91-5.76 4.87-11.27 2.3-8.71-4.1 8.44-21.25 27.39-68.49 18.43-80.02z"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8Zm48.2,326.1h-181L207.9,178h181Z"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M448.61 225.62c26.87.18 35.57-7.43 38.92-12.37 12.47-16.32-7.06-47.6-52.85-71.33 17.76-33.58 30.11-63.68 36.34-85.3 3.38-11.83 1.09-19 .45-20.25-1.72 10.52-15.85 48.46-48.2 100.05-25-11.22-56.52-20.1-93.77-23.8-8.94-16.94-34.88-63.86-60.48-88.93C252.18 7.14 238.7 1.07 228.18.22h-.05c-13.83-1.55-22.67 5.85-27.4 11-17.2 18.53-24.33 48.87-25 84.07-7.24-12.35-17.17-24.63-28.5-25.93h-.18c-20.66-3.48-38.39 29.22-36 81.29-38.36 1.38-71 5.75-93 11.23-9.9 2.45-16.22 7.27-17.76 9.72 1-.38 22.4-9.22 111.56-9.22 5.22 53 29.75 101.82 26 93.19-9.73 15.4-38.24 62.36-47.31 97.7-5.87 22.88-4.37 37.61.15 47.14 5.57 12.75 16.41 16.72 23.2 18.26 25 5.71 55.38-3.63 86.7-21.14-7.53 12.84-13.9 28.51-9.06 39.34 7.31 19.65 44.49 18.66 88.44-9.45 20.18 32.18 40.07 57.94 55.7 74.12a39.79 39.79 0 0 0 8.75 7.09c5.14 3.21 8.58 3.37 8.58 3.37-8.24-6.75-34-38-62.54-91.78 22.22-16 45.65-38.87 67.47-69.27 122.82 4.6 143.29-24.76 148-31.64 14.67-19.88 3.43-57.44-57.32-93.69zm-77.85 106.22c23.81-37.71 30.34-67.77 29.45-92.33 27.86 17.57 47.18 37.58 49.06 58.83 1.14 12.93-8.1 29.12-78.51 33.5zM216.9 387.69c9.76-6.23 19.53-13.12 29.2-20.49 6.68 13.33 13.6 26.1 20.6 38.19-40.6 21.86-68.84 12.76-49.8-17.7zm215-171.35c-10.29-5.34-21.16-10.34-32.38-15.05a722.459 722.459 0 0 0 22.74-36.9c39.06 24.1 45.9 53.18 9.64 51.95zM279.18 398c-5.51-11.35-11-23.5-16.5-36.44 43.25 1.27 62.42-18.73 63.28-20.41 0 .07-25 15.64-62.53 12.25a718.78 718.78 0 0 0 85.06-84q13.06-15.31 24.93-31.11c-.36-.29-1.54-3-16.51-12-51.7 60.27-102.34 98-132.75 115.92-20.59-11.18-40.84-31.78-55.71-61.49-20-39.92-30-82.39-31.57-116.07 12.3.91 25.27 2.17 38.85 3.88-22.29 36.8-14.39 63-13.47 64.23 0-.07-.95-29.17 20.14-59.57a695.23 695.23 0 0 0 44.67 152.84c.93-.38 1.84.88 18.67-8.25-26.33-74.47-33.76-138.17-34-173.43 20-12.42 48.18-19.8 81.63-17.81 44.57 2.67 86.36 15.25 116.32 30.71q-10.69 15.66-23.33 32.47C365.63 152 339.1 145.84 337.5 146c.11 0 25.9 14.07 41.52 47.22a717.63 717.63 0 0 0-115.34-31.71 646.608 646.608 0 0 0-39.39-6.05c-.07.45-1.81 1.85-2.16 20.33C300 190.28 358.78 215.68 389.36 233c.74 23.55-6.95 51.61-25.41 79.57-24.6 37.31-56.39 67.23-84.77 85.43zm27.4-287c-44.56-1.66-73.58 7.43-94.69 20.67 2-52.3 21.31-76.38 38.21-75.28C267 52.15 305 108.55 306.58 111zm-130.65 3.1c.48 12.11 1.59 24.62 3.21 37.28-14.55-.85-28.74-1.25-42.4-1.26-.08 3.24-.12-51 24.67-49.59h.09c5.76 1.09 10.63 6.88 14.43 13.57zm-28.06 162c20.76 39.7 43.3 60.57 65.25 72.31-46.79 24.76-77.53 20-84.92 4.51-.2-.21-11.13-15.3 19.67-76.81zm210.06 74.8"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M232 237.2c31.8-15.2 48.4-38.2 48.4-74 0-70.6-52.6-87.8-113.3-87.8H0v354.4h171.8c64.4 0 124.9-30.9 124.9-102.9 0-44.5-21.1-77.4-64.7-89.7zM77.9 135.9H151c28.1 0 53.4 7.9 53.4 40.5 0 30.1-19.7 42.2-47.5 42.2h-79v-82.7zm83.3 233.7H77.9V272h84.9c34.3 0 56 14.3 56 50.6 0 35.8-25.9 47-57.6 47zm358.5-240.7H376V94h143.7v34.9zM576 305.2c0-75.9-44.4-139.2-124.9-139.2-78.2 0-131.3 58.8-131.3 135.8 0 79.9 50.3 134.7 131.3 134.7 61.3 0 101-27.6 120.1-86.3H509c-6.7 21.9-34.3 33.5-55.7 33.5-41.3 0-63-24.2-63-65.3h185.1c.3-4.2.6-8.7.6-13.2zM390.4 274c2.3-33.7 24.7-54.8 58.5-54.8 35.4 0 53.2 20.8 56.2 54.8H390.4z"/></svg>

After

Width:  |  Height:  |  Size: 895 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M488.6 104.1C505.3 122.2 513 143.8 511.9 169.8V372.2C511.5 398.6 502.7 420.3 485.4 437.3C468.2 454.3 446.3 463.2 419.9 464H92.02C65.57 463.2 43.81 454.2 26.74 436.8C9.682 419.4 .7667 396.5 0 368.2V169.8C.7667 143.8 9.682 122.2 26.74 104.1C43.81 87.75 65.57 78.77 92.02 78H121.4L96.05 52.19C90.3 46.46 87.42 39.19 87.42 30.4C87.42 21.6 90.3 14.34 96.05 8.603C101.8 2.868 109.1 0 117.9 0C126.7 0 134 2.868 139.8 8.603L213.1 78H301.1L375.6 8.603C381.7 2.868 389.2 0 398 0C406.8 0 414.1 2.868 419.9 8.603C425.6 14.34 428.5 21.6 428.5 30.4C428.5 39.19 425.6 46.46 419.9 52.19L394.6 78L423.9 78C450.3 78.77 471.9 87.75 488.6 104.1H488.6zM449.8 173.8C449.4 164.2 446.1 156.4 439.1 150.3C433.9 144.2 425.1 140.9 416.4 140.5H96.05C86.46 140.9 78.6 144.2 72.47 150.3C66.33 156.4 63.07 164.2 62.69 173.8V368.2C62.69 377.4 65.95 385.2 72.47 391.7C78.99 398.2 86.85 401.5 96.05 401.5H416.4C425.6 401.5 433.4 398.2 439.7 391.7C446 385.2 449.4 377.4 449.8 368.2L449.8 173.8zM185.5 216.5C191.8 222.8 195.2 230.6 195.6 239.7V273C195.2 282.2 191.9 289.9 185.8 296.2C179.6 302.5 171.8 305.7 162.2 305.7C152.6 305.7 144.7 302.5 138.6 296.2C132.5 289.9 129.2 282.2 128.8 273V239.7C129.2 230.6 132.6 222.8 138.9 216.5C145.2 210.2 152.1 206.9 162.2 206.5C171.4 206.9 179.2 210.2 185.5 216.5H185.5zM377 216.5C383.3 222.8 386.7 230.6 387.1 239.7V273C386.7 282.2 383.4 289.9 377.3 296.2C371.2 302.5 363.3 305.7 353.7 305.7C344.1 305.7 336.3 302.5 330.1 296.2C323.1 289.9 320.7 282.2 320.4 273V239.7C320.7 230.6 324.1 222.8 330.4 216.5C336.7 210.2 344.5 206.9 353.7 206.5C362.9 206.9 370.7 210.2 377 216.5H377z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M416 32H32C14.4 32 0 46.4 0 64v384c0 17.6 14.4 32 32 32h384c17.6 0 32-14.4 32-32V64c0-17.6-14.4-32-32-32zm-64 257.4c0 49.4-11.4 82.6-103.8 82.6h-16.9c-44.1 0-62.4-14.9-70.4-38.8h-.9V368H96V136h64v74.7h1.1c4.6-30.5 39.7-38.8 69.7-38.8h17.3c92.4 0 103.8 33.1 103.8 82.5v35zm-64-28.9v22.9c0 21.7-3.4 33.8-38.4 33.8h-45.3c-28.9 0-44.1-6.5-44.1-35.7v-19c0-29.3 15.2-35.7 44.1-35.7h45.3c35-.2 38.4 12 38.4 33.7z"/></svg>

After

Width:  |  Height:  |  Size: 696 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M22.2 32A16 16 0 0 0 6 47.8a26.35 26.35 0 0 0 .2 2.8l67.9 412.1a21.77 21.77 0 0 0 21.3 18.2h325.7a16 16 0 0 0 16-13.4L505 50.7a16 16 0 0 0-13.2-18.3 24.58 24.58 0 0 0-2.8-.2L22.2 32zm285.9 297.8h-104l-28.1-147h157.3l-25.2 147z"/></svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zm-141.651-35.33c4.937-32.999-20.191-50.739-54.55-62.573l11.146-44.702-27.213-6.781-10.851 43.524c-7.154-1.783-14.502-3.464-21.803-5.13l10.929-43.81-27.198-6.781-11.153 44.686c-5.922-1.349-11.735-2.682-17.377-4.084l.031-.14-37.53-9.37-7.239 29.062s20.191 4.627 19.765 4.913c11.022 2.751 13.014 10.044 12.68 15.825l-12.696 50.925c.76.194 1.744.473 2.829.907-.907-.225-1.876-.473-2.876-.713l-17.796 71.338c-1.349 3.348-4.767 8.37-12.471 6.464.271.395-19.78-4.937-19.78-4.937l-13.51 31.147 35.414 8.827c6.588 1.651 13.045 3.379 19.4 5.006l-11.262 45.213 27.182 6.781 11.153-44.733a1038.209 1038.209 0 0 0 21.687 5.627l-11.115 44.523 27.213 6.781 11.262-45.128c46.404 8.781 81.299 5.239 95.986-36.727 11.836-33.79-.589-53.281-25.004-65.991 17.78-4.098 31.174-15.792 34.747-39.949zm-62.177 87.179c-8.41 33.79-65.308 15.523-83.755 10.943l14.944-59.899c18.446 4.603 77.6 13.717 68.811 48.956zm8.417-87.667c-7.673 30.736-55.031 15.12-70.393 11.292l13.548-54.327c15.363 3.828 64.836 10.973 56.845 43.035z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M78.4 67.2C173.8-22 324.5-24 421.5 71c14.3 14.1-6.4 37.1-22.4 21.5-84.8-82.4-215.8-80.3-298.9-3.2-16.3 15.1-36.5-8.3-21.8-22.1zm98.9 418.6c19.3 5.7 29.3-23.6 7.9-30C73 421.9 9.4 306.1 37.7 194.8c5-19.6-24.9-28.1-30.2-7.1-32.1 127.4 41.1 259.8 169.8 298.1zm148.1-2c121.9-40.2 192.9-166.9 164.4-291-4.5-19.7-34.9-13.8-30 7.9 24.2 107.7-37.1 217.9-143.2 253.4-21.2 7-10.4 36 8.8 29.7zm-62.9-79l.2-71.8c0-8.2-6.6-14.8-14.8-14.8-8.2 0-14.8 6.7-14.8 14.8l-.2 71.8c0 8.2 6.6 14.8 14.8 14.8s14.8-6.6 14.8-14.8zm71-269c2.1 90.9 4.7 131.9-85.5 132.5-92.5-.7-86.9-44.3-85.5-132.5 0-21.8-32.5-19.6-32.5 0v71.6c0 69.3 60.7 90.9 118 90.1 57.3.8 118-20.8 118-90.1v-71.6c0-19.6-32.5-21.8-32.5 0z"/></svg>

After

Width:  |  Height:  |  Size: 970 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M0 32v448h448V32H0zm316.5 325.2L224 445.9l-92.5-88.7 64.5-184-64.5-86.6h184.9L252 173.2l64.5 184z"/></svg>

After

Width:  |  Height:  |  Size: 388 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M166 116.9c0 23.4-16.4 49.1-72.5 49.1H23.4l21-88.8h67.8c42.1 0 53.8 23.3 53.8 39.7zm126.2-39.7h-67.8L205.7 166h70.1c53.8 0 70.1-25.7 70.1-49.1.1-16.4-11.6-39.7-53.7-39.7zM88.8 208.1H21L0 296.9h70.1c56.1 0 72.5-23.4 72.5-49.1 0-16.3-11.7-39.7-53.8-39.7zm180.1 0h-67.8l-18.7 88.8h70.1c53.8 0 70.1-23.4 70.1-49.1 0-16.3-11.7-39.7-53.7-39.7zm189.3-53.8h-67.8l-18.7 88.8h70.1c53.8 0 70.1-23.4 70.1-49.1.1-16.3-11.6-39.7-53.7-39.7zm-28 137.9h-67.8L343.7 381h70.1c56.1 0 70.1-23.4 70.1-49.1 0-16.3-11.6-39.7-53.7-39.7zM240.8 346H173l-18.7 88.8h70.1c56.1 0 70.1-25.7 70.1-49.1.1-16.3-11.6-39.7-53.7-39.7z"/></svg>

After

Width:  |  Height:  |  Size: 887 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M446.6 222.7c-1.8-8-6.8-15.4-12.5-18.5-1.8-1-13-2.2-25-2.7-20.1-.9-22.3-1.3-28.7-5-10.1-5.9-12.8-12.3-12.9-29.5-.1-33-13.8-63.7-40.9-91.3-19.3-19.7-40.9-33-65.5-40.5-5.9-1.8-19.1-2.4-63.3-2.9-69.4-.8-84.8.6-108.4 10C45.9 59.5 14.7 96.1 3.3 142.9 1.2 151.7.7 165.8.2 246.8c-.6 101.5.1 116.4 6.4 136.5 15.6 49.6 59.9 86.3 104.4 94.3 14.8 2.7 197.3 3.3 216 .8 32.5-4.4 58-17.5 81.9-41.9 17.3-17.7 28.1-36.8 35.2-62.1 4.9-17.6 4.5-142.8 2.5-151.7zm-322.1-63.6c7.8-7.9 10-8.2 58.8-8.2 43.9 0 45.4.1 51.8 3.4 9.3 4.7 13.4 11.3 13.4 21.9 0 9.5-3.8 16.2-12.3 21.6-4.6 2.9-7.3 3.1-50.3 3.3-26.5.2-47.7-.4-50.8-1.2-16.6-4.7-22.8-28.5-10.6-40.8zm191.8 199.8l-14.9 2.4-77.5.9c-68.1.8-87.3-.4-90.9-2-7.1-3.1-13.8-11.7-14.9-19.4-1.1-7.3 2.6-17.3 8.2-22.4 7.1-6.4 10.2-6.6 97.3-6.7 89.6-.1 89.1-.1 97.6 7.8 12.1 11.3 9.5 31.2-4.9 39.4z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M162.4 196c4.8-4.9 6.2-5.1 36.4-5.1 27.2 0 28.1.1 32.1 2.1 5.8 2.9 8.3 7 8.3 13.6 0 5.9-2.4 10-7.6 13.4-2.8 1.8-4.5 1.9-31.1 2.1-16.4.1-29.5-.2-31.5-.8-10.3-2.9-14.1-17.7-6.6-25.3zm61.4 94.5c-53.9 0-55.8.2-60.2 4.1-3.5 3.1-5.7 9.4-5.1 13.9.7 4.7 4.8 10.1 9.2 12 2.2 1 14.1 1.7 56.3 1.2l47.9-.6 9.2-1.5c9-5.1 10.5-17.4 3.1-24.4-5.3-4.7-5-4.7-60.4-4.7zm223.4 130.1c-3.5 28.4-23 50.4-51.1 57.5-7.2 1.8-9.7 1.9-172.9 1.8-157.8 0-165.9-.1-172-1.8-8.4-2.2-15.6-5.5-22.3-10-5.6-3.8-13.9-11.8-17-16.4-3.8-5.6-8.2-15.3-10-22C.1 423 0 420.3 0 256.3 0 93.2 0 89.7 1.8 82.6 8.1 57.9 27.7 39 53 33.4c7.3-1.6 332.1-1.9 340-.3 21.2 4.3 37.9 17.1 47.6 36.4 7.7 15.3 7-1.5 7.3 180.6.2 115.8 0 164.5-.7 170.5zm-85.4-185.2c-1.1-5-4.2-9.6-7.7-11.5-1.1-.6-8-1.3-15.5-1.7-12.4-.6-13.8-.8-17.8-3.1-6.2-3.6-7.9-7.6-8-18.3 0-20.4-8.5-39.4-25.3-56.5-12-12.2-25.3-20.5-40.6-25.1-3.6-1.1-11.8-1.5-39.2-1.8-42.9-.5-52.5.4-67.1 6.2-27 10.7-46.3 33.4-53.4 62.4-1.3 5.4-1.6 14.2-1.9 64.3-.4 62.8 0 72.1 4 84.5 9.7 30.7 37.1 53.4 64.6 58.4 9.2 1.7 122.2 2.1 133.7.5 20.1-2.7 35.9-10.8 50.7-25.9 10.7-10.9 17.4-22.8 21.8-38.5 3.2-10.9 2.9-88.4 1.7-93.9z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z"/></svg>

After

Width:  |  Height:  |  Size: 717 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M196.48 260.023l92.626-103.333L143.125 0v206.33l-86.111-86.111-31.406 31.405 108.061 108.399L25.608 368.422l31.406 31.405 86.111-86.111L145.84 512l148.552-148.644-97.912-103.333zm40.86-102.996l-49.977 49.978-.338-100.295 50.315 50.317zM187.363 313.04l49.977 49.978-50.315 50.316.338-100.294z"/></svg>

After

Width:  |  Height:  |  Size: 582 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M292.6 171.1L249.7 214l-.3-86 43.2 43.1m-43.2 219.8l43.1-43.1-42.9-42.9-.2 86zM416 259.4C416 465 344.1 512 230.9 512S32 465 32 259.4 115.4 0 228.6 0 416 53.9 416 259.4zm-158.5 0l79.4-88.6L211.8 36.5v176.9L138 139.6l-27 26.9 92.7 93-92.7 93 26.9 26.9 73.8-73.8 2.3 170 127.4-127.5-83.9-88.7z"/></svg>

After

Width:  |  Height:  |  Size: 581 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M333.5,201.4c0-22.1-15.6-34.3-43-34.3h-50.4v71.2h42.5C315.4,238.2,333.5,225,333.5,201.4z M517,188.6 c-9.5-30.9-10.9-68.8-9.8-98.1c1.1-30.5-22.7-58.5-54.7-58.5H123.7c-32.1,0-55.8,28.1-54.7,58.5c1,29.3-0.3,67.2-9.8,98.1 c-9.6,31-25.7,50.6-52.2,53.1v28.5c26.4,2.5,42.6,22.1,52.2,53.1c9.5,30.9,10.9,68.8,9.8,98.1c-1.1,30.5,22.7,58.5,54.7,58.5h328.7 c32.1,0,55.8-28.1,54.7-58.5c-1-29.3,0.3-67.2,9.8-98.1c9.6-31,25.7-50.6,52.1-53.1v-28.5C542.7,239.2,526.5,219.6,517,188.6z M300.2,375.1h-97.9V136.8h97.4c43.3,0,71.7,23.4,71.7,59.4c0,25.3-19.1,47.9-43.5,51.8v1.3c33.2,3.6,55.5,26.6,55.5,58.3 C383.4,349.7,352.1,375.1,300.2,375.1z M290.2,266.4h-50.1v78.4h52.3c34.2,0,52.3-13.7,52.3-39.5 C344.7,279.6,326.1,266.4,290.2,266.4z"/></svg>

After

Width:  |  Height:  |  Size: 1006 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M86.344,197.834a51.767,51.767,0,0,0-41.57,20.058V156.018a8.19,8.19,0,0,0-8.19-8.19H8.19A8.19,8.19,0,0,0,0,156.018V333.551a8.189,8.189,0,0,0,8.19,8.189H36.584a8.189,8.189,0,0,0,8.19-8.189v-8.088c11.628,13.373,25.874,19.769,41.573,19.769,34.6,0,61.922-26.164,61.922-73.843C148.266,225.452,121.229,197.834,86.344,197.834ZM71.516,305.691c-9.593,0-21.221-4.942-26.745-12.5V250.164c5.528-7.558,17.152-12.791,26.745-12.791,17.734,0,31.107,13.082,31.107,34.013C102.623,292.609,89.25,305.691,71.516,305.691Zm156.372-59.032a17.4,17.4,0,1,0,17.4,17.4A17.4,17.4,0,0,0,227.888,246.659ZM273.956,156.7V112.039a13.308,13.308,0,1,0-10.237,0V156.7a107.49,107.49,0,1,0,10.237,0Zm85.993,107.367c0,30.531-40.792,55.281-91.112,55.281s-91.111-24.75-91.111-55.281,40.792-55.281,91.111-55.281S359.949,233.532,359.949,264.062Zm-50.163,17.4a17.4,17.4,0,1,0-17.4-17.4h0A17.4,17.4,0,0,0,309.786,281.466ZM580.7,250.455c-14.828-2.617-22.387-3.78-22.387-9.885,0-5.523,7.268-9.884,17.735-9.884a65.56,65.56,0,0,1,34.484,10.1,8.171,8.171,0,0,0,11.288-2.468c.07-.11.138-.221.2-.333l8.611-14.886a8.2,8.2,0,0,0-2.867-11.123,99.863,99.863,0,0,0-52.014-14.138c-38.956,0-60.179,21.514-60.179,46.225,0,36.342,33.725,41.864,57.563,45.642,13.373,2.326,24.13,4.361,24.13,11.048,0,6.4-5.523,10.757-18.9,10.757-13.552,0-30.994-6.222-42.623-13.579a8.206,8.206,0,0,0-11.335,2.491c-.035.054-.069.108-.1.164l-10.2,16.891a8.222,8.222,0,0,0,2.491,11.066c15.224,10.3,37.663,16.692,59.441,16.692,40.409,0,63.957-19.769,63.957-46.515C640,260.63,604.537,254.816,580.7,250.455Zm-95.928,60.787a8.211,8.211,0,0,0-9.521-5.938,23.168,23.168,0,0,1-4.155.387c-7.849,0-12.5-6.106-12.5-14.245V240.28h20.349a8.143,8.143,0,0,0,8.141-8.143V209.466a8.143,8.143,0,0,0-8.141-8.143H458.594V171.091a8.143,8.143,0,0,0-8.143-8.143H422.257a8.143,8.143,0,0,0-8.143,8.143h0v30.232H399a8.143,8.143,0,0,0-8.143,8.143h0v22.671A8.143,8.143,0,0,0,399,240.28h15.115v63.667c0,27.037,15.408,41.282,43.9,41.282,12.183,0,21.383-2.2,27.6-5.446a8.161,8.161,0,0,0,4.145-9.278Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M298 0c3 0 5.8 1.3 7.8 3.6l38.1 44c.5-.1 1-.2 1.5-.3c9.2-1.6 18.6-2.2 27.7-1.2c11.6 1.4 21.5 5.4 28.9 12.9c7.7 7.8 15.4 15.8 22.6 23.6c2.5 2.7 4.9 5.2 6.9 7.4c.7 .8 1.4 1.5 1.9 2c3.4 3.7 4.2 8.1 2.7 11.9l-9.8 24.6 13.1 38.1c.7 2 .8 4.1 .2 6.2c-.1 .4-.1 .4-.5 2.1c-.6 2.3-.6 2.3-1.5 5.8c-1.6 6.3-3.5 13.3-5.4 20.9c-5.6 21.6-11.2 43.2-16.4 63.4c-12.9 49.9-21.4 82.7-23.4 90.9c-11.1 44.5-19.9 60-48.3 80.3c-24.9 17.8-76.8 53.6-86.8 60c-1 .6-2 1.3-3.4 2.3c-.5 .4-3.2 2.2-3.9 2.7c-4.9 3.3-8.3 5.5-12.1 7.3c-4.7 2.2-9.3 3.5-13.9 3.5s-9.1-1.2-13.9-3.5c-3.7-1.8-7.2-3.9-12.1-7.3c-.8-.5-3.4-2.4-3.9-2.7c-1.4-1-2.5-1.7-3.4-2.3c-10-6.4-61.9-42.1-86.8-60c-28.4-20.4-37.2-35.8-48.3-80.3c-2-8.2-10.5-41-23.3-90.5c-5.3-20.6-10.9-42.2-16.5-63.8c-2-7.6-3.8-14.6-5.4-20.9c-.9-3.5-.9-3.5-1.5-5.8c-.4-1.7-.4-1.7-.5-2.1c-.5-2-.4-4.2 .2-6.2l13.1-38.1L11.8 104c-1.5-3.8-.7-8.2 2-11.2c1.2-1.3 1.8-2 2.6-2.8c2-2.2 4.4-4.7 6.9-7.4C30.6 74.9 38.3 66.9 46 59.1c7.4-7.5 17.3-11.6 28.9-12.9c9.1-1.1 18.5-.5 27.7 1.2c.5 .1 1 .2 1.5 .3l38.1-44C144.2 1.3 147 0 150 0H298zm-4.7 21.1H154.7L115.6 66.2c-2.6 3-6.7 4.3-10.6 3.2c-.2-.1-.7-.2-1.5-.4c-1.3-.3-2.9-.6-4.5-.9c-7.4-1.3-14.9-1.8-21.7-1C70 68 64.3 70.3 60.7 74c-7.6 7.7-15.2 15.6-22.3 23.3c-1.7 1.8-3.3 3.5-4.8 5.1l8.8 22c1 2.4 1 5 .2 7.5L29.2 170.6c.4 1.4 .5 1.9 1.2 4.8c1.6 6.3 3.5 13.3 5.4 20.9c5.6 21.6 11.2 43.2 16.4 63.4c12.9 50 21.4 82.8 23.4 91C85.7 390.8 92 402 115.8 419c24.6 17.6 76.3 53.2 85.9 59.3c1.2 .8 2.5 1.6 4 2.7c.6 .4 3.2 2.2 3.9 2.7c4 2.8 6.7 4.4 9.2 5.6c2.2 1 3.9 1.5 5.1 1.5s2.9-.5 5.1-1.5c2.5-1.2 5.2-2.8 9.2-5.6c.7-.5 3.3-2.3 3.9-2.7c1.6-1.1 2.8-1.9 4-2.7c9.6-6.1 61.3-41.7 85.9-59.3c23.8-17.1 30.2-28.2 40.1-68.3c2.1-8.3 10.5-41.1 23.3-90.7c5.3-20.6 10.9-42.2 16.5-63.8c2-7.6 3.8-14.6 5.4-20.9c.7-2.9 .9-3.4 1.2-4.8l-13.3-38.8c-.8-2.4-.8-5.1 .2-7.5l8.8-22c-1.5-1.6-3.1-3.3-4.8-5.1c-7.2-7.6-14.7-15.5-22.3-23.3c-3.7-3.7-9.3-6-16.6-6.9c-6.8-.8-14.4-.3-21.7 1c-1.7 .3-3.2 .6-4.5 .9c-.8 .2-1.3 .3-1.5 .4c-3.8 1.1-7.9-.2-10.6-3.2L293.3 21.1zM224 316c2.8 0 20.9 6.5 35.4 14.1s25 13 28.3 15.2s1.3 6.2-1.7 8.4s-44.1 34.6-48.1 38.2s-9.8 9.5-13.8 9.5s-9.8-5.9-13.8-9.5s-45.1-36-48.1-38.2s-5.1-6.2-1.7-8.4s13.9-7.5 28.3-15.2s32.5-14.1 35.4-14.1zm.1-230.7c.7 0 8.8 .2 20.5 4.2c12.3 4.2 25.7 9.4 31.9 9.4s51.9-8.9 51.9-8.9s54.2 66.7 54.2 81s-6.8 18-13.7 25.4s-36.8 39.8-40.7 43.9s-11.9 10.5-7.1 21.8s11.7 25.8 3.9 40.4s-21 24.4-29.4 22.8s-28.4-12.2-35.7-17.1s-30.5-24.3-30.5-31.8s24-20.8 28.4-23.9s24.7-14.8 25.1-19.4s.3-6-5.7-17.4s-16.7-26.7-14.9-36.8s19.1-15.4 31.5-20.2s36.2-13.7 39.2-15.1s2.2-2.7-6.8-3.6s-34.6-4.3-46.1-1.1s-31.2 8.2-32.8 10.9s-3 2.7-1.4 11.8s10.1 52.8 10.9 60.6s2.4 12.9-5.8 14.8s-22.1 5.2-26.8 5.2s-18.6-3.3-26.8-5.2s-6.6-7-5.8-14.8s9.3-51.5 10.9-60.6s.2-9.2-1.4-11.8s-21.3-7.6-32.8-10.9s-37.1 .2-46.1 1.1s-9.8 2.2-6.8 3.6s26.8 10.4 39.2 15.1s29.7 10 31.5 20.2s-9 25.4-14.9 36.8s-6.1 12.8-5.7 17.4s20.6 16.4 25.1 19.4s28.4 16.4 28.4 23.9s-23.2 27-30.5 31.8s-27.2 15.4-35.7 17.1s-21.7-8.2-29.4-22.8s-.8-29.1 3.9-40.4s-3.3-17.7-7.1-21.8s-33.8-36.5-40.7-43.9s-13.7-11.2-13.7-25.4s54.2-81 54.2-81s45.8 8.9 51.9 8.9s19.5-5.2 31.9-9.4s20.6-4.2 20.6-4.2l.1 0z"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M145.5 0H224h78.5l44.7 50.8s39.3-10.9 57.8 7.6s33.8 34.9 33.8 34.9l-12 29.5 15.3 43.7s-44.9 170.2-50.1 191c-10.4 40.9-17.4 56.8-46.9 77.5s-82.9 56.8-91.6 62.2c-1.9 1.2-3.9 2.5-5.9 3.9c-7.5 5.1-15.8 10.8-23.5 10.8l0 0 0 0c-7.7 0-16.1-5.7-23.5-10.8c-2-1.4-4-2.8-5.9-3.9c-8.7-5.5-62.1-41.5-91.6-62.2s-36.5-36.6-46.9-77.5c-5.3-20.8-50.1-191-50.1-191l15.3-43.7L9.2 93.3s15.3-16.4 33.8-34.9s57.8-7.6 57.8-7.6L145.5 0zM224 407.6l0 0c3.7 0 8.9-4.7 13-8.4c.6-.5 1.2-1.1 1.7-1.5c4.2-3.7 47.8-37.5 51-39.8s5.4-6.5 1.9-8.7c-2.8-1.7-10-5.5-20.3-10.8c-3-1.6-6.3-3.2-9.7-5c-15.4-8-34.5-14.7-37.5-14.7l0 0 0 0c-3 0-22.1 6.8-37.5 14.7c-3.5 1.8-6.7 3.5-9.7 5c-10.3 5.3-17.6 9.1-20.3 10.8c-3.6 2.2-1.4 6.4 1.9 8.7s46.8 36.1 51 39.8c.5 .5 1.1 1 1.7 1.5c4.1 3.7 9.3 8.4 13 8.4l0 0zm0-165.7l0 0c4.7 0 17.6-3 26.4-5l0 0 2-.5c7.8-1.8 7.3-6.3 6.4-13c-.1-.8-.2-1.6-.3-2.4c-.6-6.1-5.8-33.1-9.1-50.3c-1.1-5.8-2-10.5-2.4-12.9c-1.5-8.1-.6-9.4 .7-11.3c.2-.3 .5-.7 .7-1.1c1.4-2.3 16-6.2 27.9-9.5l0 0c2.5-.7 4.8-1.3 6.9-1.9c10.6-3 32.4-.6 44.2 .6c1.8 .2 3.4 .4 4.7 .5c9.6 .9 10.4 2.3 7.2 3.8c-2.3 1.1-16.2 6.3-28.7 10.9l0 0 0 0c-4.7 1.8-9.2 3.5-12.8 4.8c-1.5 .5-3 1.1-4.5 1.7c-12.5 4.6-27.2 10-28.9 19.4c-1.5 8.3 5.2 19.9 11.3 30.3l0 0c1.6 2.8 3.2 5.5 4.6 8.1c6.3 11.9 6.5 13.3 6.1 18.1c-.4 3.9-14.5 12.7-22.4 17.6l0 0c-1.8 1.1-3.3 2.1-4.2 2.7c-.8 .5-2.1 1.4-3.8 2.4c-8.6 5.2-26.3 16-26.3 22.5c0 7.8 24.6 28.1 32.4 33.2s28.9 16.1 37.9 17.8s23-8.5 31.2-23.8c7.7-14.4 1.7-28.5-3.2-40l-.9-2.2c-4.5-10.6 1.9-17 6.2-21.3l0 0c.5-.5 1-1 1.4-1.4L377.7 194c1.3-1.3 2.5-2.6 3.7-3.8l0 0c5.8-5.7 10.8-10.5 10.8-22.8c0-14.9-57.5-84.5-57.5-84.5s-48.5 9.3-55.1 9.3c-5.2 0-15.3-3.5-25.8-7.1l0 0c-2.7-.9-5.4-1.9-8-2.7C232.8 78.1 224 78 224 78l0 0 0 0s-8.7 0-21.8 4.4c-2.7 .9-5.4 1.8-8 2.7l0 0c-10.5 3.6-20.6 7.1-25.8 7.1c-6.5 0-55.1-9.3-55.1-9.3s-57.5 69.6-57.5 84.5c0 12.3 4.9 17.1 10.8 22.8l0 0c1.2 1.2 2.5 2.4 3.7 3.8l43.1 45.8c.4 .5 .9 .9 1.4 1.4l0 0c4.3 4.3 10.6 10.7 6.2 21.3l-.9 2.2c-4.9 11.5-11 25.6-3.2 40c8.2 15.3 22.2 25.5 31.2 23.8s30.1-12.7 37.9-17.8s32.4-25.4 32.4-33.2c0-6.5-17.7-17.3-26.3-22.5c-1.7-1-3.1-1.9-3.8-2.4c-.9-.6-2.4-1.5-4.2-2.7c-7.9-4.9-22-13.7-22.4-17.6c-.4-4.8-.3-6.2 6.1-18.1c1.3-2.5 2.9-5.3 4.6-8.1c6-10.4 12.8-22 11.3-30.3c-1.7-9.4-16.4-14.8-28.9-19.4c-1.6-.6-3.1-1.1-4.5-1.7c-3.6-1.4-8.1-3.1-12.8-4.8l-.1 0c-12.5-4.7-26.4-9.9-28.7-10.9c-3.2-1.5-2.3-2.8 7.2-3.8c1.3-.1 2.9-.3 4.7-.5c11.8-1.3 33.6-3.6 44.2-.6c2.1 .6 4.4 1.2 6.9 1.9c11.9 3.2 26.5 7.2 27.9 9.5c.2 .4 .5 .7 .7 1.1c1.3 1.9 2.2 3.2 .7 11.3c-.4 2.4-1.3 7.1-2.4 12.9c-3.3 17.2-8.5 44.2-9.1 50.3c-.1 .8-.2 1.7-.3 2.4c-.8 6.7-1.4 11.2 6.4 13l2 .5 0 0c8.8 2 21.8 5 26.4 5l0 0z"/></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M310.204 242.638c27.73-14.18 45.377-39.39 41.28-81.3-5.358-57.351-52.458-76.573-114.85-81.929V0h-48.528v77.203c-12.605 0-25.525.315-38.444.63V0h-48.528v79.409c-17.842.539-38.622.276-97.37 0v51.678c38.314-.678 58.417-3.14 63.023 21.427v217.429c-2.925 19.492-18.524 16.685-53.255 16.071L3.765 443.68c88.481 0 97.37.315 97.37.315V512h48.528v-67.06c13.234.315 26.154.315 38.444.315V512h48.528v-68.005c81.299-4.412 135.647-24.894 142.895-101.467 5.671-61.446-23.32-88.862-69.326-99.89zM150.608 134.553c27.415 0 113.126-8.507 113.126 48.528 0 54.515-85.71 48.212-113.126 48.212v-96.74zm0 251.776V279.821c32.772 0 133.127-9.138 133.127 53.255-.001 60.186-100.355 53.253-133.127 53.253z"/></svg>

After

Width:  |  Height:  |  Size: 969 B

Some files were not shown because too many files have changed in this diff Show More