oauth2 revokations

This commit is contained in:
Evert Prants 2022-03-20 16:50:12 +02:00
parent 88f19163c6
commit c35cb362ff
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
36 changed files with 681 additions and 65 deletions

219
package-lock.json generated
View File

@ -16,8 +16,11 @@
"@nestjs/serve-static": "^2.2.2", "@nestjs/serve-static": "^2.2.2",
"@nestjs/throttler": "^2.0.1", "@nestjs/throttler": "^2.0.1",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"body-parser": "^1.19.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
"connect-redis": "^6.1.3",
"cookie-parser": "^1.4.6",
"cropperjs": "^1.5.12", "cropperjs": "^1.5.12",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express-session": "^1.17.2", "express-session": "^1.17.2",
@ -30,6 +33,7 @@
"otplib": "^12.0.1", "otplib": "^12.0.1",
"pug": "^3.0.2", "pug": "^3.0.2",
"qrcode": "^1.5.0", "qrcode": "^1.5.0",
"redis": "^3.1.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
@ -44,6 +48,8 @@
"@nestjs/schematics": "^8.0.0", "@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0", "@nestjs/testing": "^8.0.0",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/connect-redis": "^0.0.18",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-session": "^1.17.4", "@types/express-session": "^1.17.4",
"@types/jest": "27.4.1", "@types/jest": "27.4.1",
@ -3129,12 +3135,33 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/connect-redis": {
"version": "0.0.18",
"resolved": "https://registry.npmjs.org/@types/connect-redis/-/connect-redis-0.0.18.tgz",
"integrity": "sha512-iGygGbXgPIr94DEAuoluWhzre3c2/ew5NPlbW9IWvwCTXMM1YCmc7M9wpXMkYqt6kB9aO1sjZnmDzyugUu+2vQ==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/express-session": "*",
"@types/ioredis": "*",
"@types/redis": "^2.8.0"
}
},
"node_modules/@types/cookiejar": { "node_modules/@types/cookiejar": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
"dev": true "dev": true
}, },
"node_modules/@types/csurf": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
"dev": true,
"dependencies": {
"@types/express-serve-static-core": "*"
}
},
"node_modules/@types/eslint": { "node_modules/@types/eslint": {
"version": "8.4.1", "version": "8.4.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
@ -3202,6 +3229,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/ioredis": {
"version": "4.28.10",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz",
"integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/istanbul-lib-coverage": { "node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@ -3326,6 +3362,15 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true "dev": true
}, },
"node_modules/@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/serve-static": { "node_modules/@types/serve-static": {
"version": "1.13.10", "version": "1.13.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
@ -4986,6 +5031,14 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/connect-redis": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz",
"integrity": "sha512-aaNluLlAn/3JPxRwdzw7lhvEoU6Enb+d83xnokUNhC9dktqBoawKWL+WuxinxvBLTz6q9vReTnUDnUslaz74aw==",
"engines": {
"node": ">=12"
}
},
"node_modules/consola": { "node_modules/consola": {
"version": "2.15.3", "version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
@ -5047,6 +5100,26 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"dependencies": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -9982,6 +10055,56 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/redis": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"dependencies": {
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-redis"
}
},
"node_modules/redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/redis/node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
@ -14606,12 +14729,33 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/connect-redis": {
"version": "0.0.18",
"resolved": "https://registry.npmjs.org/@types/connect-redis/-/connect-redis-0.0.18.tgz",
"integrity": "sha512-iGygGbXgPIr94DEAuoluWhzre3c2/ew5NPlbW9IWvwCTXMM1YCmc7M9wpXMkYqt6kB9aO1sjZnmDzyugUu+2vQ==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/express-session": "*",
"@types/ioredis": "*",
"@types/redis": "^2.8.0"
}
},
"@types/cookiejar": { "@types/cookiejar": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
"integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==",
"dev": true "dev": true
}, },
"@types/csurf": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*"
}
},
"@types/eslint": { "@types/eslint": {
"version": "8.4.1", "version": "8.4.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz",
@ -14679,6 +14823,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/ioredis": {
"version": "4.28.10",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz",
"integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/istanbul-lib-coverage": { "@types/istanbul-lib-coverage": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@ -14803,6 +14956,15 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true "dev": true
}, },
"@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/serve-static": { "@types/serve-static": {
"version": "1.13.10", "version": "1.13.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
@ -16077,6 +16239,11 @@
} }
} }
}, },
"connect-redis": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz",
"integrity": "sha512-aaNluLlAn/3JPxRwdzw7lhvEoU6Enb+d83xnokUNhC9dktqBoawKWL+WuxinxvBLTz6q9vReTnUDnUslaz74aw=="
},
"consola": { "consola": {
"version": "2.15.3", "version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
@ -16131,6 +16298,22 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
}, },
"cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"requires": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
"dependencies": {
"cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
}
}
},
"cookie-signature": { "cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -19903,6 +20086,42 @@
"resolve": "^1.1.6" "resolve": "^1.1.6"
} }
}, },
"redis": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"requires": {
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
},
"dependencies": {
"denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
}
}
},
"redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
},
"reflect-metadata": { "reflect-metadata": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",

View File

@ -30,8 +30,11 @@
"@nestjs/serve-static": "^2.2.2", "@nestjs/serve-static": "^2.2.2",
"@nestjs/throttler": "^2.0.1", "@nestjs/throttler": "^2.0.1",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"body-parser": "^1.19.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
"connect-redis": "^6.1.3",
"cookie-parser": "^1.4.6",
"cropperjs": "^1.5.12", "cropperjs": "^1.5.12",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express-session": "^1.17.2", "express-session": "^1.17.2",
@ -44,6 +47,7 @@
"otplib": "^12.0.1", "otplib": "^12.0.1",
"pug": "^3.0.2", "pug": "^3.0.2",
"qrcode": "^1.5.0", "qrcode": "^1.5.0",
"redis": "^3.1.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
@ -58,6 +62,8 @@
"@nestjs/schematics": "^8.0.0", "@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0", "@nestjs/testing": "^8.0.0",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/connect-redis": "^0.0.18",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-session": "^1.17.4", "@types/express-session": "^1.17.4",
"@types/jest": "27.4.1", "@types/jest": "27.4.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -3,7 +3,7 @@
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background-color: #005b74; background-color: var(--main-darker);
margin: 2rem -4rem; margin: 2rem -4rem;
padding: 2rem 1rem; padding: 2rem 1rem;
box-shadow: inset 0px 6px 62px -14px rgba(0, 0, 0, 0.45); box-shadow: inset 0px 6px 62px -14px rgba(0, 0, 0, 0.45);
@ -22,7 +22,12 @@
width: 120px; width: 120px;
height: 120px; height: 120px;
flex-shrink: 0; flex-shrink: 0;
background-color: #b5b5b5; background-color: var(--main-darker);
img {
max-width: 100%;
max-height: 100%;
}
} }
&-content { &-content {

View File

@ -8,8 +8,8 @@
.center-box { .center-box {
max-width: 800px; max-width: 800px;
background-color: #2e6b81; background-color: var(--main);
color: #fff; color: var(--text-color);
margin: 2rem auto; margin: 2rem auto;
padding: 4rem; padding: 4rem;
position: relative; position: relative;
@ -22,8 +22,8 @@
} }
&-addon { &-addon {
color: #fff; color: var(--text-color);
background-color: #042b3a; background-color: var(--main-dark);
overflow: hidden; overflow: hidden;
max-width: 600px; max-width: 600px;
margin: 0 auto 2rem auto; margin: 0 auto 2rem auto;

View File

@ -5,7 +5,7 @@
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
outline: 0px solid #00c0ff8a; outline: 0px solid var(--focus-outline);
background-color: var(--btn-background); background-color: var(--btn-background);
color: var(--btn-color); color: var(--btn-color);
@ -14,18 +14,23 @@
&-link { &-link {
font-size: 1rem; font-size: 1rem;
color: #fff; color: var(--text-color);
text-shadow: var(--text-shadow) 1px 1px 2px;
padding: 12px; padding: 12px;
}
text-shadow: none; text-decoration: underline;
&:hover {
text-decoration: none;
}
}
&:hover { &:hover {
background-color: var(--btn-background-hover); background-color: var(--btn-background-hover);
} }
&:focus { &:focus {
outline: 4px solid #00c0ff8a; outline: 4px solid var(--focus-outline);
} }
&-primary { &-primary {

18
src/fe/scss/_colors.scss Normal file
View File

@ -0,0 +1,18 @@
:root {
--black: #000;
--white: #fff;
--text-color: var(--white);
--text-shadow: var(--black);
--form-border: #707070;
--form-border-hover: #5c5c5c;
--main-background: #314550;
--main: #2e6b81;
--main-light: #519eb9;
--main-darker: #005b74;
--main-dark: #042b3a;
--focus-outline: #00c0ff8a;
}

View File

@ -19,16 +19,16 @@ input.form-control {
padding: 8px; padding: 8px;
font-size: 1rem; font-size: 1rem;
border-radius: 4px; border-radius: 4px;
border: 1px solid #707070; border: 1px solid var(--form-border);
transition: outline 0.15s linear; transition: outline 0.15s linear;
&:focus { &:focus {
outline: 4px solid #00c0ff8a; outline: 4px solid var(--focus-outline);
} }
&:hover, &:hover,
&:focus { &:focus {
border: 1px solid #5c5c5c; border: 1px solid var(--form-border-hover);
} }
} }

View File

@ -12,7 +12,7 @@
&__content { &__content {
max-width: 800px; max-width: 800px;
background-color: #2e6b81; background-color: var(--main);
box-shadow: 0px 6px 62px -14px rgba(0, 0, 0, 0.45); box-shadow: 0px 6px 62px -14px rgba(0, 0, 0, 0.45);
margin: 4rem auto 0 auto; margin: 4rem auto 0 auto;
} }
@ -36,7 +36,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
border-top: 1px solid #005b74; border-top: 1px solid var(--main-darker);
gap: 1rem; gap: 1rem;
} }
@ -46,7 +46,7 @@
padding: 1rem; padding: 1rem;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid #005b74; border-bottom: 1px solid var(--main-darker);
&-button .btn { &-button .btn {
min-width: initial; min-width: initial;

View File

@ -8,7 +8,7 @@
&__nav { &__nav {
padding: 2rem 0rem; padding: 2rem 0rem;
background-color: #005b74; background-color: var(--main-darker);
ul { ul {
display: flex; display: flex;
@ -31,13 +31,13 @@
text-decoration: underline; text-decoration: underline;
} }
&.active, &.active {
&:focus-visible { border-right-color: var(--main-light);
border-right-color: #519eb9; font-weight: bold;
} }
&:focus { &:focus {
outline: 4px solid #00c0ff8a; outline: 4px solid var(--focus-outline);
} }
} }
} }
@ -56,6 +56,7 @@
height: 120px; height: 120px;
width: 120px; width: 120px;
flex: 0 0 120px; flex: 0 0 120px;
background-color: var(--main-darker);
margin: auto; margin: auto;
} }
} }
@ -78,7 +79,64 @@
#crop-result { #crop-result {
max-width: 100%; max-width: 100%;
max-height: 50vh; max-height: 50vh;
background-color: #005b74; background-color: var(--main-darker);
}
}
.alert {
margin-bottom: 1rem;
}
.authorization {
&__client {
display: flex;
flex-direction: row;
min-height: 120px;
margin-bottom: 1rem;
&-image {
width: 120px;
height: 120px;
flex-shrink: 0;
background-color: var(--main-darker);
img {
max-width: 100%;
max-height: 100%;
}
}
&-content {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 0 8px;
}
&-title {
font-size: 1.5rem;
}
&-urls {
margin-top: auto;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
}
&-url {
margin-top: 2px;
}
&-description {
margin-bottom: 4px;
}
@include break-on(xs, down) {
flex-direction: column;
align-items: center;
gap: 1rem;
}
} }
} }
} }

View File

@ -1,4 +1,5 @@
@import 'breakpoint'; @import 'breakpoint';
@import 'colors';
@import 'block'; @import 'block';
@import 'form'; @import 'form';
@import 'button'; @import 'button';
@ -23,12 +24,12 @@ body {
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #314550; background-color: var(--main-background);
text-shadow: black 1px 1px 2px; text-shadow: var(--text-shadow) 1px 1px 2px;
} }
a { a {
color: #fff; color: var(--text-color);
&:hover { &:hover {
text-decoration: none; text-decoration: none;

View File

@ -44,6 +44,10 @@ export class AvatarModal extends Modal {
public initialize(): void { public initialize(): void {
super.initialize(); super.initialize();
if (!this.modal) {
return;
}
this.csrf = (document.querySelector('#csrf') as HTMLInputElement).value; this.csrf = (document.querySelector('#csrf') as HTMLInputElement).value;
this.stages = this.modal?.querySelectorAll( this.stages = this.modal?.querySelectorAll(
'[data-upload-step]', '[data-upload-step]',
@ -131,7 +135,7 @@ export class AvatarModal extends Modal {
this.uploadBtn.addEventListener('click', () => { this.uploadBtn.addEventListener('click', () => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', this.cropResultBlob); formData.append('file', this.cropResultBlob);
formData.append('csrf', this.csrf); formData.append('_csrf', this.csrf);
// TODO: error // TODO: error
fetch('/account/avatar', { fetch('/account/avatar', {

View File

@ -1,6 +1,11 @@
const isVisible = (elem: HTMLElement) =>
!!elem &&
!!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
export class Modal { export class Modal {
public triggers?: NodeListOf<HTMLElement>; public triggers?: NodeListOf<HTMLElement>;
public modal?: HTMLElement; public modal?: HTMLElement;
public modalContent?: HTMLElement;
protected focusLock: HTMLElement[] = []; protected focusLock: HTMLElement[] = [];
protected trigger?: HTMLElement; protected trigger?: HTMLElement;
@ -13,6 +18,7 @@ export class Modal {
this.modal.style.display = 'none'; this.modal.style.display = 'none';
this.removeFocusLock(); this.removeFocusLock();
this.removeClickListener();
} }
public open(): void { public open(): void {
@ -22,6 +28,10 @@ export class Modal {
this.modal.style.display = 'block'; this.modal.style.display = 'block';
this.createFocusLock(); this.createFocusLock();
setTimeout(() =>
document.addEventListener('click', this.outsideClickListener),
);
} }
public initialize(): void { public initialize(): void {
@ -44,12 +54,28 @@ export class Modal {
if (this.modal) { if (this.modal) {
const attrLabel = `modal_${this.name}_label`; const attrLabel = `modal_${this.name}_label`;
const label = this.modal.querySelector('.modal__title'); const label = this.modal.querySelector('.modal__title');
this.modalContent = this.modal.querySelector('.modal__content');
this.modal.setAttribute('aria-modal', 'true'); this.modal.setAttribute('aria-modal', 'true');
this.modal.setAttribute('role', 'dialog');
this.modal.setAttribute('aria-labelledby', attrLabel); this.modal.setAttribute('aria-labelledby', attrLabel);
label.setAttribute('id', attrLabel); label.setAttribute('id', attrLabel);
} }
} }
private outsideClickListener = (event: Event) => {
if (
!this.modalContent.contains(event.target as HTMLElement) &&
isVisible(this.modalContent)
) {
this.reset();
this.removeClickListener();
}
};
private removeClickListener = () => {
document.removeEventListener('click', this.outsideClickListener);
};
private getFocusable(): HTMLElement[] { private getFocusable(): HTMLElement[] {
const focusable = Array.from( const focusable = Array.from(
this.modal.querySelectorAll( this.modal.querySelectorAll(

View File

@ -14,6 +14,12 @@ export class ModalManager {
this.close(); this.close();
}); });
}); });
window.addEventListener('keyup', (evt: KeyboardEvent) => {
if (evt.key === 'Escape') {
this.close();
}
});
} }
public register<T extends Modal>(item: T): T { public register<T extends Modal>(item: T): T {

View File

@ -2,6 +2,8 @@ import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import * as session from 'express-session'; import * as session from 'express-session';
import * as connectRedis from 'connect-redis';
import * as redis from 'redis';
import { join } from 'path'; import { join } from 'path';
import { NestExpressApplication } from '@nestjs/platform-express'; import { NestExpressApplication } from '@nestjs/platform-express';
@ -9,11 +11,22 @@ dotenv.config();
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule);
const RedisStore = connectRedis(session);
const redisClient = redis.createClient({
host: 'localhost',
port: 6379,
});
// app.use(express.urlencoded());
// app.use(cookieParser());
app.use( app.use(
session({ session({
secret: process.env.SESSION_SECRET, secret: process.env.SESSION_SECRET,
resave: true, resave: true,
saveUninitialized: false, saveUninitialized: false,
store: new RedisStore({ client: redisClient }),
cookie: { cookie: {
sameSite: 'lax', sameSite: 'lax',
secure: process.env.NODE_ENV === 'production', secure: process.env.NODE_ENV === 'production',

View File

@ -7,6 +7,7 @@ export class CSRFMiddleware implements NestMiddleware {
constructor(private readonly tokenService: TokenService) {} constructor(private readonly tokenService: TokenService) {}
use(req: Request, res: Response, next: NextFunction) { use(req: Request, res: Response, next: NextFunction) {
// TODO: do not store in session, keep the amount of pointless sessions down
if (!req.session.csrf) { if (!req.session.csrf) {
req.session.csrf = this.tokenService.generateString(64); req.session.csrf = this.tokenService.generateString(64);
} }

View File

@ -9,7 +9,7 @@ export class ValidateCSRFMiddleware implements NestMiddleware {
return next(); return next();
} }
if (req.body.csrf !== req.session.csrf) { if (req.body._csrf !== req.session.csrf) {
return next(new Error('Invalid session')); return next(new Error('Invalid session'));
} }

View File

@ -35,11 +35,8 @@ export class LoginController {
@Get() @Get()
@Render('login/login') @Render('login/login')
public loginView( public loginView(@Req() req: Request): Record<string, any> {
@Session() session: SessionData, return this.formUtil.populateTemplate(req);
@Req() req: Request,
): Record<string, any> {
return this.formUtil.populateTemplate(req, session);
} }
@Post() @Post()
@ -232,14 +229,14 @@ export class LoginController {
} }
res.render('login/password', { res.render('login/password', {
...this.formUtil.populateTemplate(req, session), ...this.formUtil.populateTemplate(req),
token: true, token: true,
}); });
return; return;
} }
res.render('login/password', { res.render('login/password', {
...this.formUtil.populateTemplate(req, session), ...this.formUtil.populateTemplate(req),
token: false, token: false,
}); });
} }

View File

@ -44,7 +44,11 @@ export class UserAdapter implements OAuth2UserAdapter {
clientId: string, clientId: string,
scope: string | string[], scope: string | string[],
): Promise<boolean> { ): Promise<boolean> {
return false; return this._service.clientService.hasAuthorized(
userId,
clientId,
this._service.splitScope(scope),
);
} }
async consent( async consent(
@ -52,6 +56,13 @@ export class UserAdapter implements OAuth2UserAdapter {
clientId: string, clientId: string,
scope: string | string[], scope: string | string[],
): Promise<boolean> { ): Promise<boolean> {
const client = await this._service.clientService.getById(clientId);
const user = await this._service.userService.getById(userId);
await this._service.clientService.createAuthorization(
user,
client,
this._service.splitScope(scope),
);
return true; return true;
} }
} }

View File

@ -64,11 +64,15 @@ export class OAuth2Service {
} }
} }
public splitScope(scope: string): string[] { public splitScope(scope: string | string[]): string[] {
if (!scope) { if (!scope) {
return []; return [];
} }
if (Array.isArray(scope)) {
return scope;
}
return scope.includes(',') return scope.includes(',')
? scope.split(',').map((item) => item.trim()) ? scope.split(',').map((item) => item.trim())
: scope.split(' '); : scope.split(' ');

View File

@ -25,11 +25,8 @@ export class RegisterController {
@Get() @Get()
@Render('register') @Render('register')
public registerView( public registerView(@Req() req: Request): Record<string, any> {
@Session() session: SessionData, return this.formUtil.populateTemplate(req);
@Req() req: Request,
): Record<string, any> {
return this.formUtil.populateTemplate(req, session);
} }
@Post() @Post()

View File

@ -1,19 +1,25 @@
import { import {
BadRequestException, BadRequestException,
Body,
Controller, Controller,
Delete,
Get, Get,
Param,
Post, Post,
Redirect, Redirect,
Render, Render,
Req, Req,
Res,
Session, Session,
UnauthorizedException,
UploadedFile, UploadedFile,
UseInterceptors, UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { Request } from 'express'; import { Request, Response } from 'express';
import { SessionData } from 'express-session'; import { SessionData } from 'express-session';
import { unlink } from 'fs/promises'; import { unlink } from 'fs/promises';
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
import { UploadService } from 'src/modules/objects/upload/upload.service'; import { UploadService } from 'src/modules/objects/upload/upload.service';
import { UserService } from 'src/modules/objects/user/user.service'; import { UserService } from 'src/modules/objects/user/user.service';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service'; import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
@ -26,6 +32,7 @@ export class SettingsController {
private readonly _form: FormUtilityService, private readonly _form: FormUtilityService,
private readonly _upload: UploadService, private readonly _upload: UploadService,
private readonly _user: UserService, private readonly _user: UserService,
private readonly _client: OAuth2ClientService,
) {} ) {}
@Get() @Get()
@ -36,8 +43,42 @@ export class SettingsController {
@Get('general') @Get('general')
@Render('settings/general') @Render('settings/general')
public general(@Req() req: Request, @Session() sess: SessionData) { public general(@Req() req: Request) {
return this._form.populateTemplate(req, sess, { user: req.user }); return this._form.populateTemplate(req, { user: req.user });
}
@Post('general')
public async updateDisplayName(
@Req() req: Request,
@Res() res: Response,
@Body() body: { display_name?: string },
) {
try {
const { display_name } = body;
if (!display_name) {
throw new Error('Display name is required.');
}
if (display_name.length < 3 || display_name.length > 32) {
throw new Error(
'Display name must be between 3 and 32 characters long.',
);
}
req.user.display_name = display_name;
await this._user.updateUser(req.user);
req.flash('message', {
error: false,
text: 'Display name has been changed!',
});
} catch (e: any) {
req.flash('message', {
error: true,
text: e.message,
});
}
res.redirect('/account/general');
} }
@Post('avatar') @Post('avatar')
@ -72,4 +113,55 @@ export class SettingsController {
file: upload.file, file: upload.file,
}; };
} }
@Post('avatar/delete')
public async deleteUserAvatar(@Req() req: Request, @Res() res: Response) {
this._user.deleteAvatar(req.user);
req.flash('message', {
error: false,
text: 'Avatar removed successfully.',
});
res.redirect('/account/general');
}
@Get('oauth2')
@Render('settings/oauth2')
public async authorizations(@Req() req: Request) {
const authorizations = await this._client.getAuthorizations(req.user);
return this._form.populateTemplate(req, { authorizations });
}
@Post('oauth2/revoke/:id')
public async revokeAuthorization(
@Req() req: Request,
@Res() res: Response,
@Param('id') id: number,
) {
const getAuth = await this._client.getAuthorization(req.user, id);
const jsreq =
req.header('content-type').startsWith('application/json') ||
req.header('accept').startsWith('application/json');
if (!getAuth) {
if (jsreq) {
throw new UnauthorizedException(
'Unauthorized or invalid revokation request',
);
}
req.flash('message', {
error: true,
text: 'Unauthorized revokation.',
});
res.redirect('/account/oauth2');
return;
}
await this._client.revokeAuthorization(getAuth);
if (jsreq) {
return res.json({ success: true });
}
res.redirect('/account/oauth2');
}
} }

View File

@ -18,6 +18,7 @@ import { UserModule } from 'src/modules/objects/user/user.module';
import { OAuth2Module } from '../oauth2/oauth2.module'; import { OAuth2Module } from '../oauth2/oauth2.module';
import { SettingsController } from './settings.controller'; import { SettingsController } from './settings.controller';
import { SettingsService } from './settings.service'; import { SettingsService } from './settings.service';
import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module';
@Module({ @Module({
controllers: [SettingsController], controllers: [SettingsController],
@ -26,6 +27,7 @@ import { SettingsService } from './settings.service';
UploadModule, UploadModule,
UserModule, UserModule,
OAuth2Module, OAuth2Module,
OAuth2ClientModule,
MulterModule.registerAsync({ MulterModule.registerAsync({
imports: [ConfigurationModule], imports: [ConfigurationModule],
useFactory: async (config: ConfigurationService) => { useFactory: async (config: ConfigurationService) => {

View File

@ -42,7 +42,7 @@ export class TwoFactorController {
const qrcode = await this.qr.createQRDataURI(url); const qrcode = await this.qr.createQRDataURI(url);
res.render('two-factor/activate', { res.render('two-factor/activate', {
...this.form.populateTemplate(req, session), ...this.form.populateTemplate(req),
qrcode, qrcode,
}); });

View File

@ -1,5 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Repository } from 'typeorm'; import { DeleteResult, Repository } from 'typeorm';
import { User } from '../user/user.entity';
import { OAuth2ClientAuthorization } from './oauth2-client-authorization.entity'; import { OAuth2ClientAuthorization } from './oauth2-client-authorization.entity';
import { import {
OAuth2ClientURL, OAuth2ClientURL,
@ -18,6 +19,98 @@ export class OAuth2ClientService {
private clientAuthRepository: Repository<OAuth2ClientAuthorization>, private clientAuthRepository: Repository<OAuth2ClientAuthorization>,
) {} ) {}
public async hasAuthorized(
userId: number,
clientId: string,
scope: string[],
): Promise<boolean> {
const authorization = await this.clientAuthRepository.findOne({
where: {
user: {
id: userId,
},
client: {
client_id: clientId,
},
},
relations: ['user', 'client'],
});
if (!authorization) {
return false;
}
// Scopes must have been allowed
const splitScope = authorization.scope.split(' ');
if (scope.every((item) => splitScope.includes(item))) {
return true;
}
return false;
}
public async createAuthorization(
user: User,
client: OAuth2Client,
scope: string[],
): Promise<OAuth2ClientAuthorization> {
const existing = await this.clientAuthRepository.findOne({
where: {
user,
client,
},
relations: ['user', 'client'],
});
if (existing) {
const splitScope = existing.scope.split(' ');
scope.forEach((item) => {
if (!splitScope.includes(item)) {
splitScope.push(item);
}
});
existing.scope = splitScope.join(' ');
await this.clientAuthRepository.save(existing);
return existing;
}
const authorization = new OAuth2ClientAuthorization();
authorization.user = user;
authorization.client = client;
authorization.scope = scope.join(' ');
await this.clientAuthRepository.insert(authorization);
return authorization;
}
public async getAuthorizations(
user: User,
): Promise<OAuth2ClientAuthorization[]> {
return this.clientAuthRepository.find({
relations: ['user', 'client', 'client.urls'],
where: { user },
});
}
public async revokeAuthorization(
auth: OAuth2ClientAuthorization,
): Promise<OAuth2ClientAuthorization> {
console.log(auth);
return this.clientAuthRepository.remove(auth);
}
public async getAuthorization(
user: User,
authId: number,
): Promise<OAuth2ClientAuthorization> {
return this.clientAuthRepository.findOne({
where: {
user,
id: authId,
},
relations: ['user'],
});
}
public async getById(id: string | number): Promise<OAuth2Client> { public async getById(id: string | number): Promise<OAuth2Client> {
let client: OAuth2Client; let client: OAuth2Client;

View File

@ -77,6 +77,12 @@ export class UserService {
return user; return user;
} }
public async deleteAvatar(user: User): Promise<void> {
if (user.picture) {
await this.upload.delete(user.picture);
}
}
public async comparePasswords( public async comparePasswords(
hash: string, hash: string,
password: string, password: string,

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
import { SessionData } from 'express-session';
@Injectable() @Injectable()
export class FormUtilityService { export class FormUtilityService {
@ -25,7 +24,6 @@ export class FormUtilityService {
*/ */
public populateTemplate( public populateTemplate(
req: Request, req: Request,
session: SessionData,
additional: Record<string, any> = {}, additional: Record<string, any> = {},
): Record<string, any> { ): Record<string, any> {
const message = req.flash('message')[0] || {}; const message = req.flash('message')[0] || {};
@ -35,7 +33,7 @@ export class FormUtilityService {
return { return {
path: req.originalUrl, path: req.originalUrl,
csrf: session.csrf, csrf: req.session.csrf,
message, message,
form, form,
...additional, ...additional,

View File

@ -12,12 +12,20 @@ block body
.authorize .authorize
.authorize__user .authorize__user
.authorize__user-image .authorize__user-image
if user.picture
img(src='/uploads/' + user.picture.file, alt=user.username)
else
img(src='/public/image/avatar.png', alt='No picture')
.authorize__user-content .authorize__user-content
span.authorize__user-title #{user.display_name} span.authorize__user-title #{user.display_name}
span.authorize__user-user @#{user.username} span.authorize__user-user @#{user.username}
.authorize__center .authorize__center
.authorize__client .authorize__client
.authorize__client-image .authorize__client-image
if client.picture
img(src='/uploads/' + client.picture.file, alt=client.title)
else
img(src='/public/image/application.png', alt='No picture')
.authorize__client-content .authorize__client-content
span.authorize__client-title #{client.title} span.authorize__client-title #{client.title}
span.authorize__client-description #{client.description} span.authorize__client-description #{client.description}
@ -45,12 +53,12 @@ block body
form(method="POST", action="") form(method="POST", action="")
div.form-container div.form-container
input(type="hidden", name="csrf", value=csrf) input(type="hidden", name="_csrf", value=csrf)
input(type="hidden", name="decision", value="1") input(type="hidden", name="decision", value="1")
button.btn.btn-primary(type="submit") Authorize button.btn.btn-primary(type="submit") Authorize
form(method="POST", action="") form(method="POST", action="")
div.form-container div.form-container
input(type="hidden", name="csrf", value=csrf) input(type="hidden", name="_csrf", value=csrf)
input(type="hidden", name="decision", value="0") input(type="hidden", name="decision", value="0")
button.btn.btn-link(type="submit") Reject button.btn.btn-link(type="submit") Reject

View File

@ -18,7 +18,7 @@ block body
form(method="post") form(method="post")
div.form-container div.form-container
input#csrf(type="hidden", name="csrf", value=csrf) input#csrf(type="hidden", name="_csrf", value=csrf)
label.form-label(for="username") Username label.form-label(for="username") Username
input.form-control#username(type="text", name="username", placeholder="Username", autofocus, value=form.username) input.form-control#username(type="text", name="username", placeholder="Username", autofocus, value=form.username)
label.form-label(for="password") Password label.form-label(for="password") Password

View File

@ -19,7 +19,7 @@ block body
form(method="post") form(method="post")
div.form-container div.form-container
input#csrf(type="hidden", name="csrf", value=csrf) input#csrf(type="hidden", name="_csrf", value=csrf)
label.form-label(for="password") New password label.form-label(for="password") New password
input.form-control#password(type="password", name="password", autofocus, placeholder="Password") input.form-control#password(type="password", name="password", autofocus, placeholder="Password")
small.form-hint Must be at least 8 characters long, contain a capital and lowercase letter and a number. small.form-hint Must be at least 8 characters long, contain a capital and lowercase letter and a number.
@ -31,7 +31,7 @@ block body
p If you have forgotten your password, please enter your accounts email address and we will send you a link to recover it. p If you have forgotten your password, please enter your accounts email address and we will send you a link to recover it.
form(method="post") form(method="post")
div.form-container div.form-container
input#csrf(type="hidden", name="csrf", value=csrf) input#csrf(type="hidden", name="_csrf", value=csrf)
label.form-label(for="email") Email address label.form-label(for="email") Email address
input.form-control#email(type="email", name="email", autofocus, placeholder="Email addres") input.form-control#email(type="email", name="email", autofocus, placeholder="Email addres")
button.btn.btn-primary(type="submit") Send recovery email button.btn.btn-primary(type="submit") Send recovery email

View File

@ -18,7 +18,7 @@ block body
form(method="post") form(method="post")
div.form-container div.form-container
input#csrf(type="hidden", name="csrf", value=csrf) input#csrf(type="hidden", name="_csrf", value=csrf)
label.form-label(for="totp") Code label.form-label(for="totp") Code
input.form-control#totp(type="text", name="totp", autofocus, placeholder="xxxxxx") input.form-control#totp(type="text", name="totp", autofocus, placeholder="xxxxxx")
button.btn.btn-primary(type="submit") Log in button.btn.btn-primary(type="submit") Log in

View File

@ -18,7 +18,7 @@ block body
form(method="post") form(method="post")
div.form-container div.form-container
input#csrf(type="hidden", name="csrf", value=csrf) input#csrf(type="hidden", name="_csrf", value=csrf)
label.form-label(for="username") Username label.form-label(for="username") Username
input.form-control#username(type="text", name="username", placeholder="Username", autofocus, value=form.username) input.form-control#username(type="text", name="username", placeholder="Username", autofocus, value=form.username)

View File

@ -16,7 +16,7 @@ block settings
.col .col
form(method="post") form(method="post")
div.form-container div.form-container
input#csrf(type="hidden", name="csrf", value=csrf) input#csrf(type="hidden", name="_csrf", value=csrf)
label.form-label(for="username") Username label.form-label(for="username") Username
input.form-control#username(type="text", name="username", placeholder="Username", disabled, value=user.username) input.form-control#username(type="text", name="username", placeholder="Username", disabled, value=user.username)
label.form-label(for="display_name") Display Name label.form-label(for="display_name") Display Name
@ -33,16 +33,18 @@ block settings
.flex-column(data-script="flex") .flex-column(data-script="flex")
button.btn.btn-primary(data-modal-trigger="avatar") Change avatar button.btn.btn-primary(data-modal-trigger="avatar") Change avatar
if user.picture if user.picture
button.btn.btn-link#remove-avatar Remove avatar form(method="post", action="/account/avatar/delete")
input(type="hidden", name="_csrf", value=csrf)
button.btn.btn-link#remove-avatar(type="submit") Remove avatar
form(method="post", data-noscript, action="/account/avatar", enctype="multipart/form-data") form(method="post", data-noscript, action="/account/avatar", enctype="multipart/form-data")
div.form-container div.form-container
input#csrf(type="hidden", name="csrf", value=csrf) input(type="hidden", name="_csrf", value=csrf)
label.form-label(for="image") Image label.form-label(for="image") Image
input.form-control#image(type="file", name="file") input.form-control#image(type="file", name="file")
small.form-hint Must be less than 10 MB and 1:1 aspect ratio. Enable JavaScript to custom crop your image. small.form-hint Must be less than 10 MB and 1:1 aspect ratio. Enable JavaScript to custom crop your image.
button.btn.btn-primary(type="submit") Change button.btn.btn-primary(type="submit") Change
.modal#avatar-modal(data-modal="avatar", aria-live="polite", role="modal", style="display: none") .modal#avatar-modal(data-modal="avatar", style="display: none")
.modal__content .modal__content
.modal__title .modal__title
|Change avatar |Change avatar

View File

@ -7,10 +7,10 @@ block body
nav.sidebar.settings__nav nav.sidebar.settings__nav
ul ul
li li
a(href="/account/general") General a(href="/account/general", class=path === '/account/general' ? 'active' : '') General
li li
a(href="/account/oauth2") Authorizations a(href="/account/oauth2", class=path === '/account/oauth2' ? 'active' : '') Authorizations
li li
a(href="/account/security") Security a(href="/account/security", class=path === '/account/security' ? 'active' : '') Security
section.content.settings__content section.content.settings__content
block settings block settings

44
views/settings/oauth2.pug Normal file
View File

@ -0,0 +1,44 @@
extends ./layout.pug
block title
|Authorizations - Account settings | Icy Network
block settings
h1 Authorizations
if message.text
if message.error
.alert.alert-danger
span #{message.text}
else
.alert.alert-success
span #{message.text}
p These applications are authorized automatically when requested, provided you have already consented to the information they require.
p By revoking the authorization, you may be prompted to authorize the application again in the future. This does NOT ensure that your information
|is deleted by any third-party applications in question. Please contact each application's owner individually if you wish to remove your information
|from their servers.
hr
.authorization-wrapper
each auth in authorizations
.authorization__client
.authorization__client-image
if auth.client.picture
img(src='/uploads/' + auth.client.picture.file, alt=auth.client.title)
else
img(src='/public/image/application.png', alt='No picture')
.authorization__client-content
span.authorization__client-title #{auth.client.title}
span.authorization__client-description #{auth.client.description}
.authorization__client-urls
each url in auth.client.urls
if url.type == 'website'
a.authorization__client-url(href=url.url, target="_blank", rel="nofollow") Visit website
if url.type == 'privacy'
a.authorization__client-url(href=url.url, target="_blank", rel="nofollow") Privacy Policy
if url.type == 'terms'
a.authorization__client-url(href=url.url, target="_blank", rel="nofollow") Terms of Service
.authorization__client-actions
form(method="post",action="/account/oauth2/revoke/" + auth.id)
input(type="hidden", name="_csrf", value=csrf)
button.btn.btn-primary(type="submit") Revoke
if !authorizations.length
p.text-center You have not authorized any applications.

View File

@ -21,7 +21,7 @@ block body
form(method="post",autocomplete="off") form(method="post",autocomplete="off")
div.form-container div.form-container
input#csrf(type="hidden", name="csrf", value=csrf) input#csrf(type="hidden", name="_csrf", value=csrf)
label.form-label(for="code") Code from authenticator app label.form-label(for="code") Code from authenticator app
input.form-control#code(type="text", name="code", autofocus, placeholder="xxxxxx") input.form-control#code(type="text", name="code", autofocus, placeholder="xxxxxx")
button.btn.btn-primary(type="submit") Activate button.btn.btn-primary(type="submit") Activate