So ya, lots of shit
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.env
|
||||
node_modules
|
||||
config
|
||||
logs
|
||||
api/.env
|
||||
api/node_modules
|
||||
certs
|
||||
130
2weekmail.fyi
Normal 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
@ -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
@ -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
@ -0,0 +1,2 @@
|
||||
.env
|
||||
node_modules
|
||||
14
api/Dockerfile
Normal 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
@ -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}`);
|
||||
});
|
||||
220
api/controllers/AuthController.js
Normal 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();
|
||||
99
api/controllers/BaseController.js
Normal 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;
|
||||
296
api/controllers/DomainController.js
Normal 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();
|
||||
320
api/controllers/MailboxController.js
Normal 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();
|
||||
512
api/controllers/MessageController.js
Normal 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();
|
||||
434
api/controllers/StatsController.js
Normal 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
@ -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
@ -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;
|
||||
25
api/db/migrations/20250318123324_user_table.js
Normal 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');
|
||||
};
|
||||
29
api/db/migrations/20250318185713_invite_table.js
Normal 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');
|
||||
}
|
||||
});
|
||||
};
|
||||
39
api/db/migrations/20250319184446_alias.js
Normal 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');
|
||||
};
|
||||
40
api/db/migrations/20250319184447_domain.js
Normal 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');
|
||||
};
|
||||
44
api/db/migrations/20250319184447_mailbox.js
Normal 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');
|
||||
};
|
||||
19
api/db/migrations/20250319191505_add_cloudflare_to_domain.js
Normal 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');
|
||||
});
|
||||
};
|
||||
19
api/db/migrations/20250319200857_add_expires_to_mailbox.js
Normal 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
@ -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
@ -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;
|
||||
49
api/db/models/BaseModel.js
Normal 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
@ -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;
|
||||
56
api/db/models/DomainAdmin.js
Normal 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;
|
||||
93
api/db/models/FetchMail.js
Normal 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
@ -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
@ -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
@ -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;
|
||||
46
api/db/seeds/domain_seeder.js
Normal 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);
|
||||
};
|
||||
22
api/db/seeds/user_seeder.js
Normal 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
@ -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
@ -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
47
api/package.json
Normal 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
6
api/public/css/bootstrap.min.css
vendored
Executable file
3096
api/public/css/metadata/categories.yml
Normal file
118512
api/public/css/metadata/icon-families.json
Normal file
56887
api/public/css/metadata/icon-families.yml
Normal file
98572
api/public/css/metadata/icons.json
Normal file
46917
api/public/css/metadata/icons.yml
Normal file
4052
api/public/css/metadata/shims.json
Normal file
646
api/public/css/metadata/shims.yml
Normal 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
|
||||
1495
api/public/css/metadata/sponsors.yml
Normal file
1493
api/public/css/sprites/brands.svg
Executable file
|
After Width: | Height: | Size: 500 KiB |
497
api/public/css/sprites/regular.svg
Executable file
|
After Width: | Height: | Size: 116 KiB |
4214
api/public/css/sprites/solid.svg
Executable file
|
After Width: | Height: | Size: 876 KiB |
1
api/public/css/svgs/brands/42-group.svg
Executable 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 |
1
api/public/css/svgs/brands/500px.svg
Executable 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 |
1
api/public/css/svgs/brands/accessible-icon.svg
Executable 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 |
1
api/public/css/svgs/brands/accusoft.svg
Executable 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 |
1
api/public/css/svgs/brands/adn.svg
Executable 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 |
1
api/public/css/svgs/brands/adversal.svg
Executable 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 |
1
api/public/css/svgs/brands/affiliatetheme.svg
Executable 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 |
1
api/public/css/svgs/brands/airbnb.svg
Executable 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 |
1
api/public/css/svgs/brands/algolia.svg
Executable 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 |
1
api/public/css/svgs/brands/alipay.svg
Executable 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 |
1
api/public/css/svgs/brands/amazon-pay.svg
Executable 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 |
1
api/public/css/svgs/brands/amazon.svg
Executable 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 |
1
api/public/css/svgs/brands/amilia.svg
Executable 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 |
1
api/public/css/svgs/brands/android.svg
Executable 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 |
1
api/public/css/svgs/brands/angellist.svg
Executable 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 |
1
api/public/css/svgs/brands/angrycreative.svg
Executable 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 |
1
api/public/css/svgs/brands/angular.svg
Executable 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 |
1
api/public/css/svgs/brands/app-store-ios.svg
Executable 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 |
1
api/public/css/svgs/brands/app-store.svg
Executable 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 |
1
api/public/css/svgs/brands/apper.svg
Executable 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 |
1
api/public/css/svgs/brands/apple-pay.svg
Executable 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 |
1
api/public/css/svgs/brands/apple.svg
Executable 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 |
1
api/public/css/svgs/brands/artstation.svg
Executable 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 |
1
api/public/css/svgs/brands/asymmetrik.svg
Executable 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 |
1
api/public/css/svgs/brands/atlassian.svg
Executable 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 |
1
api/public/css/svgs/brands/audible.svg
Executable 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 |
1
api/public/css/svgs/brands/autoprefixer.svg
Executable 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 |
1
api/public/css/svgs/brands/avianex.svg
Executable 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 |
1
api/public/css/svgs/brands/aviato.svg
Executable 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 |
1
api/public/css/svgs/brands/aws.svg
Executable 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 |
1
api/public/css/svgs/brands/bandcamp.svg
Executable 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 |
1
api/public/css/svgs/brands/battle-net.svg
Executable 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 |
1
api/public/css/svgs/brands/behance.svg
Executable 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 |
1
api/public/css/svgs/brands/bilibili.svg
Executable 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 |
1
api/public/css/svgs/brands/bimobject.svg
Executable 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 |
1
api/public/css/svgs/brands/bitbucket.svg
Executable 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 |
1
api/public/css/svgs/brands/bitcoin.svg
Executable 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 |
1
api/public/css/svgs/brands/bity.svg
Executable 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 |
1
api/public/css/svgs/brands/black-tie.svg
Executable 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 |
1
api/public/css/svgs/brands/blackberry.svg
Executable 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 |
1
api/public/css/svgs/brands/blogger-b.svg
Executable 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 |
1
api/public/css/svgs/brands/blogger.svg
Executable 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 |
1
api/public/css/svgs/brands/bluesky.svg
Executable 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 |
1
api/public/css/svgs/brands/bluetooth-b.svg
Executable 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 |
1
api/public/css/svgs/brands/bluetooth.svg
Executable 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 |
1
api/public/css/svgs/brands/bootstrap.svg
Executable 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 |
1
api/public/css/svgs/brands/bots.svg
Executable 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 |
1
api/public/css/svgs/brands/brave-reverse.svg
Executable 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 |
1
api/public/css/svgs/brands/brave.svg
Executable 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 |
1
api/public/css/svgs/brands/btc.svg
Executable 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 |