feat: inital workings of service

This commit is contained in:
Kakious 2024-07-18 21:59:27 -04:00
parent a71c33c085
commit 64ce205296
29 changed files with 975 additions and 22 deletions

View file

@ -10,14 +10,14 @@
"service": "waterwolf-auth",
// The optional 'workspaceFolder' property is the path VS Code should open by default when
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"workspaceFolder": "/app/",
"features": {
"ghcr.io/cirolosapio/devcontainers-features/alpine-git:0": {}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000, "mysql:3306"],
"forwardPorts": [3000, "mysql:3306", "redis:6379"],
"portsAttributes": {
"3000": {

View file

@ -13,7 +13,7 @@ services:
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- ..:/workspaces:cached
- ..:/app:cached
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
# cap_add:

View file

@ -1 +1 @@
['THIS IS A SECURE THING']
["secure_token_1"]

View file

@ -0,0 +1,17 @@
{
"keys": [
{
"p": "xA4KcjSuyAXWzjyG48B_s1Svo-srjm3HbzgNzXz3vyvpkZU3XF-z0TR_Vfoct5LBO8M6Rj4OXyo0vho3v1DrGgkC_V5hMfGvov4C9S31_i5Pl0gjSC_OfScqtTGftJSrA2a9ob21IhAYrE4xtCTf2ETuKaiMY5IZ-HFKuinNI7E",
"kty": "RSA",
"q": "s0sQRM3OTslJPCAXvgYpWrg1reqaKy7RBuidH90ovHWiQOySwtTcsawek5o3tD6wF-3AUZ6iu_i9Jy4T99Ocz-aOR421GHKTU2SmLmPDpM4d7WvZXvCbddF4k1ihf5OkYYV4HSC10_6SVnSLUxf34YSlfdrRlT3DVFfBg_m9aYc",
"d": "ZG1BW0fs-Wu-q9YYM7cdX7v6XixMaFEYDd7Qv-wLK1-sns7Jl0Xd6P8cKEneneRk7oBdEpjCkHiBxsrWydn4JfDZOL4ItD7lTm6q0jq_n6W2SKYu7_0Vxmm3ohuxYuQSoTofJ4G4T_yfAU3-vpF0a-jv9izl8YXZ7H3JZJawKH9TgkpYXQXmjzzro87-JtfsVBlnH_9vhUQrsBS3Eb61voUUH58swTfD0JxnfeogRD8Y0HxuwghhO_xAcE_ZMkOV0W-b4NY6jnM1MFTeP5itHAEEo5SLwXel4eRv5r9GV4XD_2eMYnzvoeA8BVOMWYQgYRanmfVvGbLZYTOmqLxNoQ",
"e": "AQAB",
"use": "sig",
"qi": "Gk6c0VY9i3ka6XNN0PcLEpgM0nIMgfOjYyjl6u0yPSA8-gbLJEKsH1xL4iJj31102VszFpGT5kCCIXUIgcAvetB0A0xxucdAO7CDMEHJoPyNYvd4i5erHuO7Kj2i6I67qmcZGHPzK-TbE4xekVSb8E5lz38J7VTwElleQ5wa08Q",
"dp": "biaJXfMVhBIrxsGg89MSrFnXONyHI0WweF9g-ePNeh4c44uXiBHJALBjHpYgjk8ovAAK_K4e-v7GlUw7qAS5om4PvPTK3PmyOXxHgyMog3_XfeKs2ADsHcrkptrTpOymTInr3zSr0RCEHELukAzrqyHHQaaOAd9zMe_NEV0tAXE",
"alg": "RS256",
"dq": "szd0IqJp950CZGRb9yknizQZDCg2RLX-YN6BuMkToBYhwq33IWMu2zaGNdpwle4XjUOs-qkMV8KSKKjJcu8Gj1YRoHqIq9BTbYdtCW_Vr1YM2jb0yA7QBpwE35w3ilOle4mzf8Ijnq2Xz22dmsiZkcZKuhvRZVGgfx1dJTOs3t8",
"n": "iU9N4HxKzLHkNwkbMxozz2fyxC5vl_2m67nFnlI7i_L9ZBPvF2UlEzShAsheriLu4zb4b-5soKVc4L5gII-98dSz_jkO2q6I2UjUDjGKVXht1zAtUqYY5WUTHR6l6Mv2Tt24Mksk2DM5NGzYdFhEZAeP1rWphjJnRXSVXMvvx_IxC5OiQeP6JrH-LZIYzidlwcVUXQaFspkTTDPdovaf29x-NNXxbRrX2fKG7nFehCcZ2S4o6j9annrcxwzbZuOYHtHXTg8euUDs-PNmfeZVB7rGvAPeuslB4QGs45zERa9e9_fKk3YpU-6WVGU5VwaYjuRyuQVUx_L6htwOsX5rVw"
}
]
}

View file

@ -32,21 +32,26 @@
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.3.10",
"@nestjs/platform-express": "^10.3.10",
"@nestjs/terminus": "^10.2.3",
"@nestjs/typeorm": "^10.0.2",
"@opentelemetry/api": "^1.9.0",
"bullmq": "^5.9.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.5",
"hbs": "^4.2.0",
"ioredis": "^5.4.1",
"mysql2": "^3.10.2",
"nanoid": "^5.0.7",
"nestjs-otel": "^6.1.1",
"nestjs-postal-client": "^0.0.6",
"oidc-provider": "^8.5.1",
"pug": "^3.0.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20",
"typia": "^6.5.1"
"typia": "^6.5.1",
"uuid": "^10.0.0"
},
"devDependencies": {
"@nestjs/cli": "^10.4.2",
@ -90,5 +95,6 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
},
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
}

View file

@ -26,9 +26,15 @@ importers:
'@nestjs/platform-express':
specifier: ^10.3.10
version: 10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10)
'@nestjs/terminus':
specifier: ^10.2.3
version: 10.2.3(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/typeorm@10.0.2(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3))))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)))
'@nestjs/typeorm':
specifier: ^10.0.2
version: 10.0.2(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)))
'@opentelemetry/api':
specifier: ^1.9.0
version: 1.9.0
bullmq:
specifier: ^5.9.0
version: 5.9.0
@ -41,6 +47,9 @@ importers:
dotenv:
specifier: ^16.4.5
version: 16.4.5
hbs:
specifier: ^4.2.0
version: 4.2.0
ioredis:
specifier: ^5.4.1
version: 5.4.1
@ -50,6 +59,9 @@ importers:
nanoid:
specifier: ^5.0.7
version: 5.0.7
nestjs-otel:
specifier: ^6.1.1
version: 6.1.1(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))
nestjs-postal-client:
specifier: ^0.0.6
version: 0.0.6(rtrsvjp22aq2opwvwwi7bofxyi)
@ -71,6 +83,9 @@ importers:
typia:
specifier: ^6.5.1
version: 6.5.1(typescript@5.5.3)
uuid:
specifier: ^10.0.0
version: 10.0.0
devDependencies:
'@nestjs/cli':
specifier: ^10.4.2
@ -659,6 +674,54 @@ packages:
class-validator:
optional: true
'@nestjs/terminus@10.2.3':
resolution: {integrity: sha512-iX7gXtAooePcyQqFt57aDke5MzgdkBeYgF5YsFNNFwOiAFdIQEhfv3PR0G+HlH9F6D7nBCDZt9U87Pks/qHijg==}
peerDependencies:
'@grpc/grpc-js': '*'
'@grpc/proto-loader': '*'
'@mikro-orm/core': '*'
'@mikro-orm/nestjs': '*'
'@nestjs/axios': ^1.0.0 || ^2.0.0 || ^3.0.0
'@nestjs/common': ^9.0.0 || ^10.0.0
'@nestjs/core': ^9.0.0 || ^10.0.0
'@nestjs/microservices': ^9.0.0 || ^10.0.0
'@nestjs/mongoose': ^9.0.0 || ^10.0.0
'@nestjs/sequelize': ^9.0.0 || ^10.0.0
'@nestjs/typeorm': ^9.0.0 || ^10.0.0
'@prisma/client': '*'
mongoose: '*'
reflect-metadata: 0.1.x || 0.2.x
rxjs: 7.x
sequelize: '*'
typeorm: '*'
peerDependenciesMeta:
'@grpc/grpc-js':
optional: true
'@grpc/proto-loader':
optional: true
'@mikro-orm/core':
optional: true
'@mikro-orm/nestjs':
optional: true
'@nestjs/axios':
optional: true
'@nestjs/microservices':
optional: true
'@nestjs/mongoose':
optional: true
'@nestjs/sequelize':
optional: true
'@nestjs/typeorm':
optional: true
'@prisma/client':
optional: true
mongoose:
optional: true
sequelize:
optional: true
typeorm:
optional: true
'@nestjs/testing@10.3.10':
resolution: {integrity: sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==}
peerDependencies:
@ -1056,6 +1119,9 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@ -1204,6 +1270,10 @@ packages:
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
boxen@5.1.2:
resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==}
engines: {node: '>=10'}
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@ -1307,6 +1377,10 @@ packages:
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
check-disk-space@3.4.0:
resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==}
engines: {node: '>=16'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@ -1328,6 +1402,10 @@ packages:
class-validator@0.14.1:
resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==}
cli-boxes@2.2.1:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'}
cli-color@2.0.4:
resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==}
engines: {node: '>=0.10'}
@ -1920,6 +1998,9 @@ packages:
debug:
optional: true
foreachasync@3.0.0:
resolution: {integrity: sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==}
foreground-child@3.2.1:
resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==}
engines: {node: '>=14'}
@ -2044,6 +2125,11 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
handlebars@4.7.7:
resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==}
engines: {node: '>=0.4.7'}
hasBin: true
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
@ -2075,6 +2161,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hbs@4.2.0:
resolution: {integrity: sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
hexoid@1.0.0:
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
engines: {node: '>=8'}
@ -3788,6 +3878,11 @@ packages:
peerDependencies:
typescript: '>=4.8.0 <5.6.0'
uglify-js@3.18.0:
resolution: {integrity: sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==}
engines: {node: '>=0.8.0'}
hasBin: true
uid@2.0.2:
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
engines: {node: '>=8'}
@ -3823,6 +3918,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
@ -3846,6 +3945,9 @@ packages:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
walk@2.3.15:
resolution: {integrity: sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==}
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
@ -3889,6 +3991,10 @@ packages:
engines: {node: '>= 8'}
hasBin: true
widest-line@3.1.0:
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
engines: {node: '>=8'}
with@7.0.2:
resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==}
engines: {node: '>= 10.0.0'}
@ -3897,6 +4003,9 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@ -4660,6 +4769,18 @@ snapshots:
class-transformer: 0.5.1
class-validator: 0.14.1
'@nestjs/terminus@10.2.3(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/typeorm@10.0.2(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3))))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)))':
dependencies:
'@nestjs/common': 10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
boxen: 5.1.2
check-disk-space: 3.4.0
reflect-metadata: 0.2.2
rxjs: 7.8.1
optionalDependencies:
'@nestjs/typeorm': 10.0.2(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3)))
typeorm: 0.3.20(ioredis@5.4.1)(mysql2@3.10.2)(ts-node@10.9.2(@types/node@20.14.10)(typescript@5.5.3))
'@nestjs/testing@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10))':
dependencies:
'@nestjs/common': 10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@ -5116,6 +5237,10 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3
ansi-colors@4.1.3: {}
ansi-escapes@4.3.2:
@ -5282,6 +5407,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
boxen@5.1.2:
dependencies:
ansi-align: 3.0.1
camelcase: 6.3.0
chalk: 4.1.2
cli-boxes: 2.2.1
string-width: 4.2.3
type-fest: 0.20.2
widest-line: 3.1.0
wrap-ansi: 7.0.0
brace-expansion@1.1.11:
dependencies:
balanced-match: 1.0.2
@ -5403,6 +5539,8 @@ snapshots:
chardet@0.7.0: {}
check-disk-space@3.4.0: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@ -5429,6 +5567,8 @@ snapshots:
libphonenumber-js: 1.11.4
validator: 13.12.0
cli-boxes@2.2.1: {}
cli-color@2.0.4:
dependencies:
d: 1.0.2
@ -6073,6 +6213,8 @@ snapshots:
follow-redirects@1.15.6: {}
foreachasync@3.0.0: {}
foreground-child@3.2.1:
dependencies:
cross-spawn: 7.0.3
@ -6226,6 +6368,15 @@ snapshots:
graphemer@1.4.0: {}
handlebars@4.7.7:
dependencies:
minimist: 1.2.8
neo-async: 2.6.2
source-map: 0.6.1
wordwrap: 1.0.0
optionalDependencies:
uglify-js: 3.18.0
has-flag@3.0.0: {}
has-flag@4.0.0: {}
@ -6248,6 +6399,11 @@ snapshots:
dependencies:
function-bind: 1.1.2
hbs@4.2.0:
dependencies:
handlebars: 4.7.7
walk: 2.3.15
hexoid@1.0.0: {}
highlight.js@10.7.3: {}
@ -8123,6 +8279,9 @@ snapshots:
randexp: 0.5.3
typescript: 5.5.3
uglify-js@3.18.0:
optional: true
uid@2.0.2:
dependencies:
'@lukeed/csprng': 1.1.0
@ -8151,6 +8310,8 @@ snapshots:
utils-merge@1.0.1: {}
uuid@10.0.0: {}
uuid@9.0.1: {}
v8-compile-cache-lib@3.0.1: {}
@ -8167,6 +8328,10 @@ snapshots:
void-elements@3.1.0: {}
walk@2.3.15:
dependencies:
foreachasync: 3.0.0
walker@1.0.8:
dependencies:
makeerror: 1.0.12
@ -8230,6 +8395,10 @@ snapshots:
dependencies:
isexe: 2.0.0
widest-line@3.1.0:
dependencies:
string-width: 4.2.3
with@7.0.2:
dependencies:
'@babel/parser': 7.24.8
@ -8239,6 +8408,8 @@ snapshots:
word-wrap@1.2.5: {}
wordwrap@1.0.0: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0

2
src/app.const.ts Normal file
View file

@ -0,0 +1,2 @@
// This is the internal client ID for the application itself.
export const internalClientId = 'internal.management';

View file

@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';
import { PostalClientService } from 'nestjs-postal-client';
@ -10,8 +10,9 @@ export class AppController {
) {}
@Get()
getHello(): string {
return this.appService.getHello();
@Render('home/index')
home() {
return { message: 'Hello world!'}
}
@Get('email_test')

View file

@ -4,15 +4,37 @@ import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import config from './config/config';
import { MailModule } from './mail/mail.module';
import { RedisModule } from './redis/redis.module';
import { OpenTelemetryModule } from 'nestjs-otel';
@Module({
imports: [
OpenTelemetryModule.forRoot({
metrics: {
apiMetrics: {
enable: true,
ignoreRoutes: [
'/favicon.ico',
'/OidcServiceWorker.js',
'/swagger',
'/swagger-json',
'/swagger-yaml',
'/swagger/(.*)',
'/metrics',
'/interaction/(.*}',
],
ignoreUndefinedRoutes: true,
},
},
}),
ConfigModule.forRoot({
cache: true,
isGlobal: true,
load: [config],
}),
MailModule,
RedisModule,
],
controllers: [AppController],
providers: [AppService],

View file

@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View file

@ -3,4 +3,10 @@ export default async () => ({
port: process.env.PORT || 3000,
host: process.env.HOST || 'localhost',
},
redis: {
host: process.env['REDIS_HOST'] ?? 'localhost',
port: process.env['REDIS_POST'] ?? 6379,
password: process.env['REDIS_PASSWORD'] ?? '',
db: process.env['REDIS_DB'] ?? 0
}
});

View file

@ -0,0 +1,32 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from './user.model';
@Entity('api_keys')
export class ApiKey extends BaseEntity {
@PrimaryColumn()
id: string;
@PrimaryColumn()
key: string;
@Column()
title: string;
@ManyToOne(() => User, (user) => user.apiKeys)
user: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View file

@ -0,0 +1,87 @@
import type {
ClientAuthMethod,
ResponseType,
SigningAlgorithmWithNone,
} from 'oidc-provider';
import {
Column,
Entity,
JoinTable,
ManyToMany,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { OidcClientPermission } from './oidc_client_permissions.model';
import { MAX_STRING_LENGTH } from '../database.const';
@Entity()
export class OidcClient {
// Client ID
@PrimaryGeneratedColumn('uuid')
client_id: string;
// Owner Org ID
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: false })
ownerName: string;
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: false })
client_secret: string;
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: true })
client_name: string;
@Column({ type: 'simple-array', nullable: false })
redirect_uris: string[];
@Column({ type: 'simple-array', nullable: true })
client_cors: string[];
@Column({ type: 'simple-array', nullable: true })
allowed_introspection_targets: string[];
@Column({ type: 'simple-array', nullable: false })
include_permissions_from_client: string[];
@Column({ type: 'simple-array', nullable: false })
post_logout_redirect_uris: string[];
@Column({ type: 'simple-array', nullable: false })
response_types: ResponseType[];
@Column({ type: 'simple-array', nullable: false })
grant_types: string[];
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: false })
token_endpoint_auth_method: ClientAuthMethod;
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: false })
application_type: 'web' | 'native';
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: false })
logo_uri: string;
@Column({ type: 'boolean', nullable: false, default: false })
restricted: boolean;
@OneToMany(() => OidcClientPermission, (permission) => permission.client)
permissions: OidcClientPermission[];
@ManyToMany(
() => OidcClientPermission,
(permission) => permission.assignedClients,
)
@JoinTable({
name: 'oidc_client_permissions',
joinColumn: {
name: 'client_id',
},
inverseJoinColumn: {
name: 'permission_id',
},
})
assignedPermissions: OidcClientPermission[];
[key: string]: unknown;
}

View file

@ -0,0 +1,28 @@
import {
BeforeInsert,
Column,
Entity,
ManyToMany,
ManyToOne,
PrimaryColumn,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';
import { OidcClient } from './oidc_client.model';
@Unique(['title', 'client'])
@Entity('oidc_client_permission')
export class OidcClientPermission {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@ManyToMany(() => OidcClient, (oidcClient) => oidcClient.assignedPermissions)
assignedClients: OidcClient[];
@ManyToOne(() => OidcClient, (oidcClient) => oidcClient.permissions)
client: OidcClient;
}

View file

@ -0,0 +1,69 @@
import type { AdapterPayload } from 'oidc-provider';
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
import {
convertFromNumberToTime,
convertFromTimeToNumber,
} from '../../util/time.util';
@Entity()
export class OidcGrant implements AdapterPayload {
@PrimaryColumn()
id: string;
@Index('oidc_account_id')
@Column({
nullable: true,
})
accountId: string;
@Index('oidc_client_id')
@Column({
nullable: true,
})
clientId: string;
@Column({
type: 'timestamp',
nullable: true,
transformer: {
from: (value: Date) => convertFromTimeToNumber(value),
to: (value: number) => convertFromNumberToTime(value),
},
})
iat?: number;
@Column({
type: 'timestamp',
nullable: true,
transformer: {
from: (value: Date) => convertFromTimeToNumber(value),
to: (value: number) => convertFromNumberToTime(value),
},
})
exp?: number;
@Column({ type: 'json', nullable: true })
openid?: {
scope?: string;
claims?: string[];
};
@Column({ type: 'simple-array', nullable: true })
resources?: string[];
generateResponse(): AdapterPayload {
return {
iat: this.iat,
exp: this.exp,
accountId: this.accountId,
clientId: this.clientId,
kind: 'Grant',
jti: this.id,
openid: this.openid,
resources: this.resources,
};
}
[key: string]: unknown;
}

View file

@ -0,0 +1,41 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { MAX_STRING_LENGTH } from '../database.const';
@Entity('organization')
export class Organization {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: MAX_STRING_LENGTH })
name: string;
@Column({ length: MAX_STRING_LENGTH, nullable: true })
logo?: string;
@Column({ length: MAX_STRING_LENGTH, nullable: true })
background?: string;
@Column({ length: MAX_STRING_LENGTH, nullable: true })
website?: string;
@Column({ length: MAX_STRING_LENGTH, nullable: true })
description?: string;
@Column({ name: 'owner_id' })
ownerId: number;
@Column({
name: 'created_at',
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
})
createdAt: Date;
@Column({
name: 'updated_at',
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
})
updatedAt: Date;
}

View file

@ -1,5 +1,6 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { MAX_STRING_LENGTH, UserRole } from '../database.const';
import { ApiKey } from './api_keys.model';
@Entity('user')
export class User {
@ -9,9 +10,15 @@ export class User {
@Column({ length: MAX_STRING_LENGTH })
username: string;
@Column({ length: MAX_STRING_LENGTH, name: 'display_name', nullable: true })
displayName: string;
@Column({ length: MAX_STRING_LENGTH })
email: string;
@Column({ name: 'email_verified', default: false })
emailVerified: boolean;
@Column({ length: MAX_STRING_LENGTH })
password: string;
@ -25,5 +32,33 @@ export class User {
avatar: string;
@Column({ length: 15, enum: UserRole, default: UserRole.USER })
role: string;
role: UserRole;
@Column({ name: 'disabled', default: false })
disabled: boolean;
// This is for Gravatar Support
@Column({ name: 'email_hash', length: MAX_STRING_LENGTH, nullable: true })
emailHash: string;
// Relationship Mapping
@OneToMany(() => ApiKey, (apiKey) => apiKey.user)
apiKeys: ApiKey[];
// Created At/Updated At
@Column({
name: 'created_at',
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
})
createdAt: Date;
@Column({
name: 'updated_at',
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
})
updatedAt: Date;
}

View file

View file

View file

@ -1,8 +1,17 @@
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
);
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(3000);
}
bootstrap();
bootstrap();

10
src/redis/redis.module.ts Normal file
View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { RedisService } from './redis.service';
@Module({
providers: [RedisService, ConfigService],
exports: [RedisService],
})
export class RedisModule {}

286
src/redis/redis.service.ts Normal file
View file

@ -0,0 +1,286 @@
import { Injectable, Logger, OnApplicationShutdown } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HealthIndicatorResult, HealthIndicatorStatus } from '@nestjs/terminus';
import { ObservableGauge } from '@opentelemetry/api';
import Redis, { ChainableCommander } from 'ioredis';
import { MetricService } from 'nestjs-otel';
@Injectable()
export class RedisService implements OnApplicationShutdown {
private _ioredis: Redis;
private readonly logger = new Logger(RedisService.name);
private readonly serviceConnectedObservableGauge: ObservableGauge;
private readonly host: string | undefined;
private readonly port: number;
private readonly password: string | undefined;
private readonly db: number;
constructor(
configService: ConfigService,
private readonly metricService: MetricService,
) {
this.host = configService.getOrThrow('redis.host');
this.port = +configService.get('redis.port') ?? 6789;
this.password = configService.get('redis.password') ?? '';
this.db = +configService.get('redis.db') ?? 0;
this.serviceConnectedObservableGauge = this.metricService.getObservableGauge(
'service.connected',
{ description: 'Whether a needed service is connected' },
);
this.logger.debug(`Connecting to Redis at ${this.host}:${this.port} using db ${this.db}`);
this._ioredis = new Redis({
host: this.host,
port: this.port,
password: this.password,
db: this.db,
enableAutoPipelining: true,
});
this._ioredis.on('ready', () => {
this.logger.log(`Connected to Redis at ${this.host}:${this.port} using db ${this.db}`);
});
this._ioredis.on('error', (err) => {
this.logger.error(err, 'Redis error');
});
this.serviceConnectedObservableGauge.addCallback((result) => {
result.observe(this._ioredis.status === 'ready' ? 1 : 0, { target: 'redis' });
});
}
async onApplicationShutdown(): Promise<void> {
await this._ioredis.quit();
}
public get ioredis(): Redis {
return this._ioredis;
}
/**
* Store a value or object in redis
* @param key Key for the value to store
* @param value Value to store
* @param ttl Time to live in seconds
*/
public async set(key: string, value: string | object, ttl?: number): Promise<void> {
this.logger.debug(`Setting key ${key}`);
if (typeof value === 'object') {
value = JSON.stringify(value);
}
if (ttl) {
await this._ioredis.set(key, value, 'EX', ttl);
} else {
await this._ioredis.set(key, value);
}
}
/**
* Will return a string or object based on if it was stored as one. Otherwise will return null if not found.
* @param key Key for the value to get
* @returns
*/
public async get(key: string): Promise<string | object | null> {
this.logger.debug(`Getting key ${key}`);
const value = await this._ioredis.get(key);
if (!value) {
return null;
}
try {
return JSON.parse(value);
} catch (error) {
return value;
}
}
/**
* Remove a value from redis
* @param key Key for the value to remove
* @returns
*/
public async del(key: string): Promise<void> {
await this._ioredis.del(key);
}
/**
* Get then delete a value from redis
* @param key Key for the value to get and remove
* @returns
*/
public async getDel(key: string): Promise<string | object | null> {
const value = await this.get(key);
await this.del(key);
return value;
}
/**
* Create a redis multi command
* @returns Redis.Multi
*/
public multi(): ChainableCommander {
return this._ioredis.multi();
}
/**
* Execute a redis multi command
* @param multi Redis.Multi
* @returns Promise<any>
*/
public async exec(multi: ChainableCommander): Promise<any> {
return await multi.exec();
}
/**
* Set a ttl on a key
* @param key Key for the value to set the ttl on
* @param ttl Time to live in seconds
* @returns Promise<number>
*/
public async expire(key: string, ttl: number): Promise<number> {
return await this._ioredis.expire(key, ttl);
}
/**
* Set a key with a ttl
* @param key Key for the value to set
* @param value Value to set
* @param ttl Time to live in seconds
* @returns Promise<void>
*/
public async setex(key: string, value: string | object, ttl: number): Promise<void> {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
await this._ioredis.setex(key, ttl, value);
}
/**
* Get the ttl on a key
* @param key Key for the value to get the ttl on
* @returns Promise<number>
*/
public async ttl(key: string): Promise<number> {
return await this._ioredis.ttl(key);
}
/**
* Run a JSON.GET command and parse the result, return null if not found
* @param key Key for the value to get
* @returns Promise<object | null>
*/
public async jsonGet(key: string): Promise<object | null> {
const value = (await this._ioredis.call('JSON.GET', key)) as string | null;
if (!value) {
return null;
}
try {
return JSON.parse(value);
} catch (error) {
return null;
}
}
/**
* Run a JSON.SET command and stringify the value if it is an object
* @param key Key for the value to set
* @param value Value to set
* @returns Promise<void>
*/
public async jsonSet(key: string, value: string | object): Promise<void> {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
await this._ioredis.set(key, value);
}
/**
* Run a JSON.SET command with an expiration and stringify the value if it is an object
* @param key Key for the value to set
* @param value Value to set
* @param ttl Time to live in seconds
* @returns Promise<void>
*/
public async jsonSetEx(key: string, value: string | object, ttl: number): Promise<void> {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
await this._ioredis.set(key, value, 'EX', ttl);
}
/**
* Modify a value of a JSON.SET command
* @param key Key for the value to modify
* @param path Path to the value to modify
* @param value Value to set
* @returns Promise<void>
*/
public async jsonSetPath(
key: string,
path: string,
value: string | object | number,
): Promise<void> {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
await this._ioredis.call('JSON.SET', key, path, value);
}
/**
* Preform a lrange command and parse the results
* @param key Key for the value to get
* @param start Start index
* @param stop Stop index
* @param parseJson Parse the results as JSON, normally false
* @returns Promise<object[]>
*/
public async lrange(
key: string,
start: number,
stop: number,
parseJson = false,
): Promise<object[] | any[]> {
const value = await this._ioredis.lrange(key, start, stop);
if (!value) {
return [];
}
if (parseJson) {
return value.map((v) => JSON.parse(v));
}
return value;
}
/**
* Ping the redis server
*/
public async ping(): Promise<string> {
return await this._ioredis.ping();
}
/**
* Check the health of the redis connection
* @returns Promise<string> up | down
*/
public async checkHealth(): Promise<HealthIndicatorResult> {
let status: HealthIndicatorStatus = 'down';
try {
await this.ping();
status = 'up';
} catch (error) {
status = 'down';
}
return {
redis: {
status,
},
};
}
}

7
src/util/time.util.ts Normal file
View file

@ -0,0 +1,7 @@
export const convertFromNumberToTime = (time: number | null): Date | null => {
return time ? new Date(time * 1000) : null;
};
export const convertFromTimeToNumber = (time: Date | null): number | null => {
return time ? time.getTime() / 1000 : null;
};

114
views/auth/login.hbs Normal file
View file

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
.video-bg {
position: fixed;
right: 0;
bottom: 0;
min-width: 100%;
min-height: 100%;
z-index: -1;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: -1;
}
@media (max-width: 640px) {
.video-bg, .overlay {
display: none;
}
.login-prompt {
margin-left: 0;
width: 100%;
max-width: none;
position: static;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
}
}
</style>
</head>
<body class="relative flex items-center justify-start min-h-screen">
<video autoplay muted loop class="video-bg">
<source src="file:///C:/Users/kakious/Desktop/login.webm" type="video/webm">
Your browser does not support the video tag.
</video>
<div class="overlay"></div>
<div class="relative bg-gray-800 p-8 rounded-lg shadow-lg w-full max-w-md ml-16 login-prompt">
<h2 class="text-2xl font-bold mb-2 text-white text-center">Welcome back!</h2>
<p class="text-gray-400 mb-6 text-center">We're so excited to see you again!</p>
<form action="{{ login_url }}" method="POST" class="space-y-6">
<div>
<label for="email" class="block text-sm font-medium text-gray-400">EMAIL OR USERNAME</label>
<input type="text" id="email" name="email" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-400">PASSWORD</label>
<input type="password" id="password" name="password" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
</div>
<div class="flex items-center justify-between">
<div class="text-sm">
<a href="{{ forgot_password }}" class="font-medium text-indigo-500 hover:text-indigo-400">Forgot your password?</a>
</div>
</div>
<div>
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Log In</button>
</div>
</form>
<div class="mt-4 text-sm text-center">
<span class="text-gray-400">Need an account? </span><a href="{{ register }}" class="font-medium text-indigo-500 hover:text-indigo-400">Register</a>
</div>
<div class="mt-6 flex items-center justify-center text-gray-400 text-xs">
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 784.69 187.35" width="40%" height="40%">
<defs>
<style>
.cls-1 {
fill: #fff;
stroke-width: 0px;
}
</style>
</defs>
<g id="WatchingWaterwolf">
<g>
<g>
<polygon class="cls-1" points="269.31 78.37 260.14 136.91 248.7 78.37 244.28 78.37 239.92 78.37 235.38 78.37 223.91 137.03 214.7 78.37 198.81 78.37 214.82 165.36 220.98 165.36 225.1 165.36 230.48 165.36 242.08 109.33 253.48 165.36 258.92 165.36 262.92 165.36 269.19 165.36 285.15 78.37 269.31 78.37"/>
<path class="cls-1" d="M320.05,78.37h-10.52l-27.42,86.99h16.85l5.18-19.06h25.25l5.18,19.06h16.91l-27.54-86.99h-3.88ZM307.75,132.98l9.01-33.17,9.01,33.17h-18.02Z"/>
<polygon class="cls-1" points="365.7 78.37 344.13 78.37 344.13 91.76 365.7 91.76 365.7 165.36 381.59 165.36 381.59 91.76 403.52 91.76 403.52 78.37 381.59 78.37 365.7 78.37"/>
<polygon class="cls-1" points="425.51 127.07 453.59 127.07 453.59 114.1 425.51 114.1 425.51 91.76 458.43 91.76 458.43 78.37 425.51 78.37 420.91 78.37 409.5 78.37 409.5 165.36 420.91 165.36 425.51 165.36 458.67 165.36 458.67 152.04 425.51 152.04 425.51 127.07"/>
<path class="cls-1" d="M511.89,124.74c2.37-2.23,4.2-5.01,5.5-8.33,1.29-3.33,1.94-7.26,1.94-11.8,0-5.7-1.07-10.5-3.2-14.4-2.13-3.9-5.23-6.85-9.29-8.84-4.06-1.99-8.98-2.99-14.76-2.99h-27.07v86.99h16.01v-32.68h10.33l13.68,32.68h17.09v-.84l-15.69-36c2.03-1.05,3.85-2.3,5.44-3.79ZM481.03,91.76h11.05c2.67,0,4.83.56,6.48,1.67,1.65,1.12,2.87,2.72,3.64,4.81.78,2.09,1.17,4.59,1.17,7.5,0,2.67-.43,5.03-1.28,7.08-.86,2.05-2.15,3.65-3.88,4.81-1.73,1.16-3.89,1.73-6.48,1.73h-10.69v-27.6Z"/>
<polygon class="cls-1" points="584.07 136.91 572.63 78.37 568.2 78.37 563.84 78.37 559.3 78.37 547.84 137.03 538.63 78.37 522.74 78.37 538.75 165.36 544.9 165.36 549.03 165.36 554.4 165.36 566.01 109.33 577.4 165.36 582.84 165.36 586.85 165.36 593.12 165.36 609.07 78.37 593.24 78.37 584.07 136.91"/>
<path class="cls-1" d="M666.17,86.74c-2.73-3.15-5.97-5.53-9.74-7.14-3.76-1.61-7.96-2.42-12.58-2.42s-8.76.81-12.55,2.42c-3.78,1.61-7.04,3.99-9.77,7.14-2.73,3.15-4.83,7.07-6.3,11.77-1.47,4.7-2.21,10.18-2.21,16.43v14.64c0,6.13.75,11.51,2.24,16.13,1.49,4.62,3.6,8.47,6.33,11.56,2.73,3.09,5.98,5.41,9.77,6.96,3.78,1.55,7.99,2.33,12.61,2.33s8.76-.78,12.55-2.33c3.78-1.55,7.03-3.87,9.74-6.96,2.71-3.09,4.78-6.94,6.21-11.56,1.43-4.62,2.15-10,2.15-16.13v-14.64c0-6.25-.73-11.73-2.18-16.43-1.45-4.7-3.54-8.62-6.27-11.77ZM658.73,129.57c0,4.18-.31,7.77-.93,10.75-.62,2.99-1.55,5.42-2.81,7.29-1.25,1.87-2.81,3.26-4.66,4.15-1.85.9-3.97,1.34-6.36,1.34s-4.57-.45-6.42-1.34c-1.85-.9-3.43-2.28-4.72-4.15-1.29-1.87-2.26-4.3-2.9-7.29-.64-2.99-.96-6.57-.96-10.75v-14.76c0-4.26.32-7.93.96-10.99.64-3.07,1.58-5.57,2.84-7.5,1.25-1.93,2.81-3.36,4.66-4.27,1.85-.92,3.99-1.37,6.42-1.37s4.51.46,6.36,1.37c1.85.92,3.42,2.34,4.69,4.27,1.27,1.93,2.23,4.43,2.87,7.5.64,3.07.96,6.73.96,10.99v14.76Z"/>
<polygon class="cls-1" points="699.6 78.37 683.59 78.37 683.59 165.36 695 165.36 699.6 165.36 731.15 165.36 731.15 152.04 699.6 152.04 699.6 78.37"/>
<polygon class="cls-1" points="784.69 91.76 784.69 78.37 753.26 78.37 749.38 78.37 737.25 78.37 737.25 165.36 753.26 165.36 753.26 129.16 780.98 129.16 780.98 115.83 753.26 115.83 753.26 91.76 784.69 91.76"/>
<path class="cls-1" d="M206.63,54.66h6.38c2.79,0,5.18-.54,7.17-1.62,1.99-1.08,3.53-2.6,4.61-4.56,1.08-1.96,1.62-4.31,1.62-7.04s-.54-4.98-1.62-7.03c-1.08-2.05-2.61-3.66-4.61-4.84-1.99-1.17-4.38-1.76-7.17-1.76h-14.12v42.04h7.74v-15.19ZM213.01,34.28c1.33,0,2.4.34,3.22,1.01.82.67,1.42,1.56,1.79,2.67.38,1.11.56,2.29.56,3.54s-.19,2.44-.56,3.46c-.38,1.02-.97,1.81-1.79,2.38-.82.57-1.89.85-3.22.85h-6.38v-13.92h6.38Z"/>
<path class="cls-1" d="M233.41,65.94c1.32,1.49,2.89,2.61,4.72,3.36,1.83.75,3.86,1.13,6.09,1.13s4.23-.38,6.06-1.13c1.83-.75,3.4-1.87,4.71-3.36s2.31-3.35,3-5.59c.69-2.23,1.04-4.83,1.04-7.8v-7.07c0-3.02-.35-5.67-1.05-7.94-.7-2.27-1.71-4.17-3.03-5.69-1.32-1.52-2.89-2.67-4.71-3.45-1.82-.78-3.84-1.17-6.08-1.17s-4.23.39-6.06,1.17c-1.83.78-3.4,1.93-4.72,3.45-1.32,1.52-2.33,3.42-3.05,5.69-.71,2.27-1.07,4.92-1.07,7.94v7.07c0,2.96.36,5.56,1.08,7.8.72,2.23,1.74,4.1,3.06,5.59ZM236.98,45.42c0-2.06.15-3.83.46-5.31.31-1.48.77-2.69,1.37-3.62s1.36-1.62,2.25-2.06c.9-.44,1.93-.66,3.1-.66s2.18.22,3.08.66c.9.44,1.65,1.13,2.27,2.06.62.93,1.08,2.14,1.39,3.62.31,1.48.46,3.25.46,5.31v7.13c0,2.02-.15,3.75-.45,5.2-.3,1.44-.75,2.62-1.36,3.52-.61.91-1.36,1.57-2.25,2.01s-1.92.65-3.07.65-2.21-.22-3.1-.65-1.66-1.1-2.28-2.01c-.63-.9-1.09-2.08-1.4-3.52-.31-1.44-.46-3.18-.46-5.2v-7.13Z"/>
<polygon class="cls-1" points="271.89 69.85 273.88 69.85 276.48 69.85 282.09 42.77 287.59 69.85 290.22 69.85 292.16 69.85 295.19 69.85 302.9 27.81 295.25 27.81 290.81 56.1 285.29 27.81 283.15 27.81 281.04 27.81 278.85 27.81 273.31 56.16 268.86 27.81 261.18 27.81 268.91 69.85 271.89 69.85"/>
<polygon class="cls-1" points="313.3 69.85 329.32 69.85 329.32 63.41 313.3 63.41 313.3 51.34 326.86 51.34 326.86 45.08 313.3 45.08 313.3 34.28 329.2 34.28 329.2 27.81 313.3 27.81 311.07 27.81 305.56 27.81 305.56 69.85 311.07 69.85 313.3 69.85"/>
<path class="cls-1" d="M340.12,54.06h4.99l6.61,15.79h8.26v-.4l-7.58-17.4c.98-.51,1.86-1.11,2.63-1.83,1.14-1.08,2.03-2.42,2.66-4.03.62-1.61.94-3.51.94-5.7,0-2.75-.52-5.07-1.54-6.96-1.03-1.89-2.53-3.31-4.49-4.27s-4.34-1.44-7.13-1.44h-13.08v42.04h7.74v-15.79ZM345.46,34.28c1.29,0,2.33.27,3.13.81.8.54,1.39,1.31,1.76,2.32.38,1.01.56,2.22.56,3.62,0,1.29-.21,2.43-.62,3.42-.41.99-1.04,1.77-1.88,2.32-.84.56-1.88.84-3.13.84h-5.17v-13.34h5.34Z"/>
<polygon class="cls-1" points="370.21 69.85 386.23 69.85 386.23 63.41 370.21 63.41 370.21 51.34 383.78 51.34 383.78 45.08 370.21 45.08 370.21 34.28 386.12 34.28 386.12 27.81 370.21 27.81 367.99 27.81 362.47 27.81 362.47 69.85 367.99 69.85 370.21 69.85"/>
<path class="cls-1" d="M414.99,59.71c.75-2.19,1.13-4.71,1.13-7.54v-6.67c0-2.83-.38-5.35-1.13-7.55-.75-2.2-1.83-4.06-3.25-5.56-1.42-1.5-3.1-2.64-5.07-3.42-1.96-.78-4.16-1.17-6.58-1.17h-10.83v42.04h10.65c2.48,0,4.72-.39,6.71-1.17,1.99-.78,3.7-1.92,5.11-3.42,1.41-1.5,2.5-3.35,3.25-5.54ZM408.44,52.18c0,2.62-.26,4.75-.79,6.41-.53,1.66-1.41,2.87-2.66,3.65-1.24.78-2.93,1.17-5.07,1.17h-2.92v-29.13h3.09c1.56,0,2.87.21,3.93.64,1.06.42,1.92,1.08,2.57,1.96.65.89,1.13,2.04,1.41,3.45.29,1.42.43,3.12.43,5.12v6.73Z"/>
<path class="cls-1" d="M439.51,69.85h5.83c2.73,0,5.05-.47,6.96-1.4,1.91-.93,3.36-2.31,4.36-4.14,1-1.83,1.5-4.09,1.5-6.79,0-1.69-.3-3.29-.9-4.78-.6-1.49-1.55-2.71-2.86-3.65-.64-.46-1.37-.8-2.2-1.03.33-.15.65-.31.94-.5,1.45-.92,2.54-2.11,3.25-3.55.71-1.44,1.07-3.03,1.07-4.76,0-1.96-.29-3.66-.88-5.1-.59-1.43-1.44-2.62-2.56-3.57-1.12-.94-2.47-1.64-4.07-2.09-1.6-.45-3.42-.68-5.46-.68h-12.73v42.04h7.74ZM449.91,60.57c-.37.9-.92,1.59-1.66,2.09s-1.71.75-2.9.75h-5.83v-12.21h6.12c1.17,0,2.11.26,2.81.77.7.51,1.21,1.24,1.53,2.18.32.94.48,2.05.48,3.32,0,1.17-.18,2.21-.55,3.1ZM444.51,34.28c1.23,0,2.23.19,3,.58.77.39,1.33.99,1.69,1.8.36.82.53,1.9.53,3.25,0,1.19-.2,2.2-.59,3.03-.4.83-.98,1.46-1.76,1.89-.78.43-1.75.65-2.9.65h-4.97v-11.2h4.99Z"/>
<polygon class="cls-1" points="469.2 69.85 476.99 69.85 476.99 54.49 488.46 27.81 479.97 27.81 473.1 47.24 466.2 27.81 457.74 27.81 469.2 54.49 469.2 69.85"/>
</g>
<g id="svgg">
<path id="path0" class="cls-1" d="M80.35,1.12C23.73,10.38-12.01,65.53,3.72,119.36c2.66,9.11,2.57,9.09,4.65.98,1.01-3.96,2.15-7.97,2.51-8.91.46-1.16.46-3.35,0-6.88C1.7,33.58,80.91-15.25,140.59,24.61c62.01,41.42,41.22,138.57-32.4,151.41-9.96,1.74-9.44,1.91-17.14-5.73-7.48-7.42-7.33-7.49-4.35,2.04l1.5,4.82-4.37-.47c-6.64-.71-16.51-3.4-18.16-4.95-4.5-4.23-8.03-17.49-8.49-31.9-.16-5.13-.52-9.35-.8-9.37-3.57-.26-12.18,3.82-18.23,8.63-5.47,4.35-5.51,4.31-3.58-3.27,6.35-24.86,25.51-50.23,46.13-61.08l4.59-2.41-2.5-.47c-1.37-.26-5.19-.32-8.48-.13l-5.99.34,6.26-3.14c7.29-3.66,12.94-5.63,19.12-6.65,2.54-.42,4.42-1.06,4.42-1.5,0-2.75,17.52-24.58,19.72-24.58.32,0,.39,3,.17,6.71-.5,8.33-.65,8.12,9.92,13.97,9.29,5.14,10.94,6.49,10.67,8.77-.32,2.77-2.15,2.36-4.07-.92-1.99-3.39-3.89-4.57-8.77-5.46-4.07-.74-4.29-.51-3.37,3.57,1.14,5.08,4.17,7.32,12.24,9.03,9.23,1.96,24.42,8.39,23.96,10.15-.17.67.2,2,.83,2.95,1.1,1.67,1.09,1.81-.17,3.51-4.4,5.94-10.39,7.17-17.04,3.5-14.31-7.9-23.73-11.17-33.69-11.72-9.43-.51-17,1.63-9.42,2.67,3.57.49,7.31,2.31,9.46,4.63l1.59,1.7h-3.23c-21.98.09-38.75,19.17-33.92,38.6,1.68,6.75,6.31,15.09,12.02,21.63,2.26,2.59,2.38,2.46.96-.94-3.79-9.06-1.92-25.02,3.66-31.38,4.06-4.62,4.44-4.42,4.07,2.11-1.12,19.7,12.31,32.5,37.18,35.43,4.57.54,4.82.24,1.45-1.68-13.6-7.76-23.89-25.44-21.7-37.28,2.68-14.47,11.58-19.55,31.42-17.92l9.68.79,2.88-1.69c6.92-4.05,15.9-14.15,14.15-15.91-.53-.53-6.4-3.31-13.04-6.18l-12.07-5.22-.33-4.1c-.41-5.11-.38-5.07-6.27-9.04-4.45-3-4.87-3.48-4.87-5.5,0-5.65-3.19-17.27-4.52-16.45-.34.21-.82,3.62-1.08,7.58-.48,7.36-1.03,8.5-2.98,6.14-.62-.75-.88-3.38-.9-9.1-.02-8.59-.79-10.81-3.75-10.81-4.6,0-18.11,10.78-26.81,21.39-1.4,1.71-2.77,2.35-7.85,3.71-20.14,5.36-47.11,21.68-54.6,33.04-2.46,3.73-2.09,3.82,3.62.86,11.63-6.03,24.03-9.95,17.65-5.59-18.72,12.81-30.43,34.04-34.32,62.23-.94,6.84,20.19,26.42,37.06,34.32,69.91,32.76,147.48-28.19,131.88-103.62C175.69,26.85,127.77-6.64,80.35,1.12"/>
</g>
</g>
</g>
</svg>
</div>
</div>
</body>
</html>

10
views/home/index.hbs Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>App</title>
</head>
<body>
{{ message }}
</body>
</html>