mirror of
				https://github.com/CorentinTh/it-tools.git
				synced 2025-10-31 20:13:52 +00:00 
			
		
		
		
	Compare commits
	
		
			24 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0ff853437b | ||
|  | 39c8f92065 | ||
|  | 35b5187119 | ||
|  | 94698cea50 | ||
|  | 8294cd68da | ||
|  | 7c9b8ac178 | ||
|  | 5d8f46abf8 | ||
|  | 35a3760771 | ||
|  | 4ef25887b9 | ||
|  | 7f229959d6 | ||
|  | d3a2936979 | ||
|  | 5f16885923 | ||
|  | ea5e7a7fc7 | ||
|  | 7de6c86f9e | ||
|  | 83da6b7ee9 | ||
|  | 737319edf1 | ||
|  | a77a82f5a2 | ||
|  | da17696293 | ||
|  | 164e32b442 | ||
|  | 49755909bd | ||
|  | 44d653b1f2 | ||
|  | 7c449f4f2d | ||
|  | ab7483b5c2 | ||
|  | 5222bd5d04 | 
| @@ -29,6 +29,7 @@ module.exports = { | |||||||
|       { |       { | ||||||
|         js: 'never', |         js: 'never', | ||||||
|         ts: 'never', |         ts: 'never', | ||||||
|  |         tsx: 'never', | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -7,32 +7,21 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - uses: actions/checkout@v3 | ||||||
|         uses: actions/checkout@master |       - run: corepack enable | ||||||
|         with: |       - uses: actions/setup-node@v3 | ||||||
|           fetch-depth: 0 |  | ||||||
|  |  | ||||||
|       - name: Setup node env |  | ||||||
|         uses: actions/setup-node@v3.0.0 |  | ||||||
|         with: |         with: | ||||||
|           node-version: 16 |           node-version: 16 | ||||||
|  |           cache: 'pnpm' | ||||||
|       - name: Cache node_modules |  | ||||||
|         uses: actions/cache@v2 |  | ||||||
|         with: |  | ||||||
|           path: ~/.npm |  | ||||||
|           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} |  | ||||||
|           restore-keys: | |  | ||||||
|             ${{ runner.os }}-node- |  | ||||||
|  |  | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: npm ci |         run: pnpm i | ||||||
|  |  | ||||||
|       - name: Run linters |       - name: Run linters | ||||||
|         run: npm run lint |         run: pnpm lint | ||||||
|  |  | ||||||
|       - name: Run unit test |       - name: Run unit test | ||||||
|         run: npm run test |         run: pnpm test | ||||||
|  |  | ||||||
|       - name: Build the app |       - name: Build the app | ||||||
|         run: npm run build |         run: pnpm build | ||||||
|   | |||||||
							
								
								
									
										76
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -2,6 +2,82 @@ | |||||||
|  |  | ||||||
| All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. | ||||||
|  |  | ||||||
|  | ### [2.14.1](https://github.com/CorentinTh/it-tools/compare/v2.14.0...v2.14.1) (2022-11-23) | ||||||
|  |  | ||||||
|  | ## [2.14.0](https://github.com/CorentinTh/it-tools/compare/v2.13.0...v2.14.0) (2022-11-23) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * **new-tool:** chmod calculator ([35b5187](https://github.com/CorentinTh/it-tools/commit/35b518711938c2bc88f35d104bb35d9956f0c267)) | ||||||
|  |  | ||||||
|  | ## [2.13.0](https://github.com/CorentinTh/it-tools/compare/v2.11.0...v2.13.0) (2022-11-14) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * **config:** added tsx to allowed extension ([ea5e7a7](https://github.com/CorentinTh/it-tools/commit/ea5e7a7fc7df1a3a912193912a6ab80a8a36a256)) | ||||||
|  | * **date-converter:** added mongodb objectID format ([4ef2588](https://github.com/CorentinTh/it-tools/commit/4ef25887b9d874b8789bf8dbabd8aab92b4b1b03)) | ||||||
|  | * **new-tool:** added otp generator ([5f16885](https://github.com/CorentinTh/it-tools/commit/5f168859238e9c3a8b8bbaf6b550c4b9bd163e00)) | ||||||
|  | * **new-tool:** mime type to extension converter ([7c9b8ac](https://github.com/CorentinTh/it-tools/commit/7c9b8ac178967151a4f921ac26e8c2fe8d23b886)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **ui:** remove icon transparency overlap ([35a3760](https://github.com/CorentinTh/it-tools/commit/35a376077116dd65b21f9a0786d2ecfc14db6051)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Refactors | ||||||
|  |  | ||||||
|  | * **otp-generator:** changed url ([7f22995](https://github.com/CorentinTh/it-tools/commit/7f229959d64b7a932f32753e3838d87a819a9192)) | ||||||
|  | * token generator can use a custom alphabet ([83da6b7](https://github.com/CorentinTh/it-tools/commit/83da6b7ee9db29e40faf288f9627257aa7124038)) | ||||||
|  | * **ui:** change sponsor button location and caption ([5d8f46a](https://github.com/CorentinTh/it-tools/commit/5d8f46abf8d5a10cc4650efc87b12a9a6c537fe5)) | ||||||
|  | * **useQRCode:** switched args to MaybeRef ([7de6c86](https://github.com/CorentinTh/it-tools/commit/7de6c86f9ead8d7315614cc508dfee4fed90e9c2)) | ||||||
|  |  | ||||||
|  | ## [2.12.0](https://github.com/CorentinTh/it-tools/compare/v2.10.3...v2.12.0) (2022-08-23) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * added colored share card ([ab7483b](https://github.com/CorentinTh/it-tools/commit/ab7483b5c2bd5aee1b8b609597c22b7b7b55606d)) | ||||||
|  | * **config:** added tsx to allowed extension ([741a3c2](https://github.com/CorentinTh/it-tools/commit/741a3c25a915d8296987b23bda03f2b664d51ba6)) | ||||||
|  | * **new-tool:** added otp generator ([cc6070a](https://github.com/CorentinTh/it-tools/commit/cc6070a16655bce9de90517bdda3bf6224ba139d)) | ||||||
|  | * **new-tool:** meta tag generator ([164e32b](https://github.com/CorentinTh/it-tools/commit/164e32b4428b8dfaaddcefa06b767a8af94573a9)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **deps:** added missing optional deps ([4975590](https://github.com/CorentinTh/it-tools/commit/49755909bdaea9399e51b67fbd1a6d071acd3182)) | ||||||
|  | * removed colored card border ([7c449f4](https://github.com/CorentinTh/it-tools/commit/7c449f4f2d491ce58726c5419a74dc295fa92905)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Refactors | ||||||
|  |  | ||||||
|  | * **colored-card:** added transition on like hover ([da17696](https://github.com/CorentinTh/it-tools/commit/da17696293270005b1b7ec4aafc0df7496f602c7)) | ||||||
|  | * **share:** updated share meta ([5222bd5](https://github.com/CorentinTh/it-tools/commit/5222bd5d04ad089ba4cbade399dada55e29dcde5)) | ||||||
|  | * token generator can use a custom alphabet ([59ec629](https://github.com/CorentinTh/it-tools/commit/59ec6293b65526fe8dc527ac596d0e5af29b1e32)) | ||||||
|  | * **useQRCode:** switched args to MaybeRef ([a89c9be](https://github.com/CorentinTh/it-tools/commit/a89c9bea42d598f4caba10800becd66a07bbcdc9)) | ||||||
|  |  | ||||||
|  | ## [2.11.0](https://github.com/CorentinTh/it-tools/compare/v2.10.3...v2.11.0) (2022-08-19) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * added colored share card ([ab7483b](https://github.com/CorentinTh/it-tools/commit/ab7483b5c2bd5aee1b8b609597c22b7b7b55606d)) | ||||||
|  | * **new-tool:** meta tag generator ([164e32b](https://github.com/CorentinTh/it-tools/commit/164e32b4428b8dfaaddcefa06b767a8af94573a9)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **deps:** added missing optional deps ([4975590](https://github.com/CorentinTh/it-tools/commit/49755909bdaea9399e51b67fbd1a6d071acd3182)) | ||||||
|  | * removed colored card border ([7c449f4](https://github.com/CorentinTh/it-tools/commit/7c449f4f2d491ce58726c5419a74dc295fa92905)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Refactors | ||||||
|  |  | ||||||
|  | * **colored-card:** added transition on like hover ([da17696](https://github.com/CorentinTh/it-tools/commit/da17696293270005b1b7ec4aafc0df7496f602c7)) | ||||||
|  | * **share:** updated share meta ([5222bd5](https://github.com/CorentinTh/it-tools/commit/5222bd5d04ad089ba4cbade399dada55e29dcde5)) | ||||||
|  |  | ||||||
| ### [2.10.3](https://github.com/CorentinTh/it-tools/compare/v2.10.2...v2.10.3) (2022-08-14) | ### [2.10.3](https://github.com/CorentinTh/it-tools/compare/v2.10.2...v2.10.3) (2022-08-14) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,11 +31,9 @@ | |||||||
|       property="og:description" |       property="og:description" | ||||||
|       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." |       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." | ||||||
|     /> |     /> | ||||||
|     <meta property="og:image" content="https://it-tools.tech/banner.png" /> |     <meta property="og:image" content="https://it-tools.tech/banner.png?v=2" /> | ||||||
|  |  | ||||||
|     <meta name="twitter:card" content="summary_large_image" /> |     <meta name="twitter:card" content="summary_large_image" /> | ||||||
|     <meta name="twitter:domain" content="it-tools.tech" /> |  | ||||||
|     <meta name="twitter:url" content="https://it-tools.tech/" /> |  | ||||||
|     <meta name="twitter:site" content="@ittoolsdottech" /> |     <meta name="twitter:site" content="@ittoolsdottech" /> | ||||||
|     <meta name="twitter:creator" content="@cthmsst" /> |     <meta name="twitter:creator" content="@cthmsst" /> | ||||||
|  |  | ||||||
| @@ -44,7 +42,7 @@ | |||||||
|       name="twitter:description" |       name="twitter:description" | ||||||
|       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." |       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." | ||||||
|     /> |     /> | ||||||
|     <meta name="twitter:image" content="https://it-tools.tech/banner.png" /> |     <meta name="twitter:image" content="https://it-tools.tech/banner.png?v=2" /> | ||||||
|     <meta name="twitter:image:alt" content="IT Tools - Handy online tools for developers" /> |     <meta name="twitter:image:alt" content="IT Tools - Handy online tools for developers" /> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|   | |||||||
							
								
								
									
										20581
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20581
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										68
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "it-tools", |   "name": "it-tools", | ||||||
|   "version": "2.10.3", |   "version": "2.14.1", | ||||||
|   "description": "Collection of handy online tools for developers, with great UX. ", |   "description": "Collection of handy online tools for developers, with great UX. ", | ||||||
|   "keywords": [ |   "keywords": [ | ||||||
|     "productivity", |     "productivity", | ||||||
| @@ -32,66 +32,70 @@ | |||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@it-tools/bip39": "^0.0.4", |     "@it-tools/bip39": "^0.0.4", | ||||||
|  |     "@it-tools/oggen": "^1.3.0", | ||||||
|     "@vicons/material": "^0.12.0", |     "@vicons/material": "^0.12.0", | ||||||
|     "@vicons/tabler": "^0.12.0", |     "@vicons/tabler": "^0.12.0", | ||||||
|     "@vueuse/core": "^8.2.1", |     "@vueuse/core": "^8.9.4", | ||||||
|     "@vueuse/head": "^0.7.5", |     "@vueuse/head": "^0.7.13", | ||||||
|     "bcryptjs": "^2.4.3", |     "bcryptjs": "^2.4.3", | ||||||
|     "change-case": "^4.1.2", |     "change-case": "^4.1.2", | ||||||
|     "colord": "^2.9.2", |     "colord": "^2.9.3", | ||||||
|     "cron-validator": "^1.3.1", |     "cron-validator": "^1.3.1", | ||||||
|     "cronstrue": "^2.2.0", |     "cronstrue": "^2.15.0", | ||||||
|     "crypto-js": "^4.1.1", |     "crypto-js": "^4.1.1", | ||||||
|     "date-fns": "^2.28.0", |     "date-fns": "^2.29.3", | ||||||
|     "figue": "^1.2.0", |     "figue": "^1.2.0", | ||||||
|     "highlight.js": "^11.5.1", |     "highlight.js": "^11.6.0", | ||||||
|  |     "json5": "^2.2.1", | ||||||
|     "lodash": "^4.17.21", |     "lodash": "^4.17.21", | ||||||
|     "mathjs": "^10.6.0", |     "mathjs": "^10.6.4", | ||||||
|     "mime-types": "^2.1.35", |     "mime-types": "^2.1.35", | ||||||
|     "naive-ui": "^2.31.0", |     "naive-ui": "^2.33.5", | ||||||
|     "pinia": "^2.0.11", |     "pinia": "^2.0.23", | ||||||
|     "plausible-tracker": "^0.3.5", |     "plausible-tracker": "^0.3.8", | ||||||
|     "qrcode": "^1.5.0", |     "qrcode": "^1.5.1", | ||||||
|     "randombytes": "^2.1.0", |     "randombytes": "^2.1.0", | ||||||
|     "sql-formatter": "^8.2.0", |     "sql-formatter": "^8.2.0", | ||||||
|     "uuid": "^8.3.2", |     "uuid": "^8.3.2", | ||||||
|     "vue": "^3.2.31", |     "vue": "^3.2.45", | ||||||
|     "vue-router": "^4.0.12" |     "vue-router": "^4.1.6" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@rushstack/eslint-patch": "^1.1.0", |     "@rushstack/eslint-patch": "^1.2.0", | ||||||
|     "@types/bcryptjs": "^2.4.2", |     "@types/bcryptjs": "^2.4.2", | ||||||
|     "@types/crypto-js": "^4.1.1", |     "@types/crypto-js": "^4.1.1", | ||||||
|     "@types/jsdom": "^16.2.14", |     "@types/jsdom": "^16.2.15", | ||||||
|  |     "@types/lodash": "^4.14.188", | ||||||
|     "@types/mime-types": "^2.1.1", |     "@types/mime-types": "^2.1.1", | ||||||
|     "@types/node": "^16.11.25", |     "@types/node": "^16.18.3", | ||||||
|     "@types/qrcode": "^1.4.2", |     "@types/qrcode": "^1.5.0", | ||||||
|     "@types/randombytes": "^2.0.0", |     "@types/randombytes": "^2.0.0", | ||||||
|     "@types/uuid": "^8.3.4", |     "@types/uuid": "^8.3.4", | ||||||
|     "@typescript-eslint/parser": "^5.32.0", |     "@typescript-eslint/parser": "^5.42.1", | ||||||
|     "@vitejs/plugin-vue": "^2.2.2", |     "@vitejs/plugin-vue": "^2.3.4", | ||||||
|     "@vitejs/plugin-vue-jsx": "^1.3.7", |     "@vitejs/plugin-vue-jsx": "^1.3.10", | ||||||
|     "@vue/eslint-config-prettier": "^7.0.0", |     "@vue/eslint-config-prettier": "^7.0.0", | ||||||
|     "@vue/eslint-config-typescript": "^10.0.0", |     "@vue/eslint-config-typescript": "^10.0.0", | ||||||
|     "@vue/test-utils": "^2.0.0-rc.18", |     "@vue/test-utils": "^2.2.2", | ||||||
|     "@vue/tsconfig": "^0.1.3", |     "@vue/tsconfig": "^0.1.3", | ||||||
|     "c8": "^7.11.0", |     "c8": "^7.12.0", | ||||||
|     "eslint": "^8.5.0", |     "eslint": "^8.27.0", | ||||||
|     "eslint-config-prettier": "^8.5.0", |     "eslint-config-prettier": "^8.5.0", | ||||||
|     "eslint-import-resolver-typescript": "^3.4.0", |     "eslint-import-resolver-typescript": "^3.5.2", | ||||||
|     "eslint-plugin-import": "^2.26.0", |     "eslint-plugin-import": "^2.26.0", | ||||||
|     "eslint-plugin-vue": "^8.2.0", |     "eslint-plugin-vue": "^8.7.1", | ||||||
|     "jsdom": "^19.0.0", |     "jsdom": "^19.0.0", | ||||||
|     "less": "^4.1.2", |     "less": "^4.1.3", | ||||||
|     "prettier": "^2.6.2", |     "prettier": "^2.7.1", | ||||||
|     "standard-version": "^9.3.2", |     "standard-version": "^9.5.0", | ||||||
|     "start-server-and-test": "^1.14.0", |     "start-server-and-test": "^1.14.0", | ||||||
|     "typescript": "~4.5.5", |     "typescript": "~4.5.5", | ||||||
|     "vite": "^2.9.1", |     "vite": "^2.9.15", | ||||||
|     "vite-plugin-md": "^0.12.4", |     "vite-plugin-md": "^0.12.4", | ||||||
|     "vite-plugin-pwa": "^0.11.13", |     "vite-plugin-pwa": "^0.11.13", | ||||||
|     "vite-svg-loader": "^3.2.0", |     "vite-svg-loader": "^3.6.0", | ||||||
|     "vitest": "^0.13.1", |     "vitest": "^0.13.1", | ||||||
|     "vue-tsc": "^0.31.4" |     "vue-tsc": "^0.31.4", | ||||||
|  |     "workbox-window": "^6.5.4" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										7378
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7378
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										59
									
								
								src/components/ColoredCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/ColoredCard.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | <template> | ||||||
|  |   <n-card class="colored-card"> | ||||||
|  |     <n-space justify="space-between" align="center"> | ||||||
|  |       <n-icon class="icon" size="40" :component="icon" /> | ||||||
|  |     </n-space> | ||||||
|  |     <n-h3 class="title"> | ||||||
|  |       <n-ellipsis>{{ title }}</n-ellipsis> | ||||||
|  |     </n-h3> | ||||||
|  |  | ||||||
|  |     <div class="description"> | ||||||
|  |       <n-ellipsis :line-clamp="2" :tooltip="false"> | ||||||
|  |         <slot /> | ||||||
|  |       </n-ellipsis> | ||||||
|  |     </div> | ||||||
|  |   </n-card> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { toRefs, type Component } from 'vue'; | ||||||
|  |  | ||||||
|  | const props = defineProps<{ icon: Component; title: string }>(); | ||||||
|  | const { icon, title } = toRefs(props); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="less" scoped> | ||||||
|  | .colored-card { | ||||||
|  |   background: rgb(37, 99, 108); | ||||||
|  |   background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%); | ||||||
|  |   color: #fff; | ||||||
|  |   border: none; | ||||||
|  |  | ||||||
|  |   .icon { | ||||||
|  |     opacity: 0.7; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .title { | ||||||
|  |     color: #fff; | ||||||
|  |  | ||||||
|  |     margin: 5px 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .description { | ||||||
|  |     opacity: 0.8; | ||||||
|  |  | ||||||
|  |     margin: 5px 0; | ||||||
|  |  | ||||||
|  |     ::v-deep(a) { | ||||||
|  |       color: inherit; | ||||||
|  |       text-decoration: underline; | ||||||
|  |       font-weight: bold; | ||||||
|  |       transition: color ease 0.2s; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         color: rgb(20, 20, 20); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -33,10 +33,12 @@ import { useClipboard, useElementSize } from '@vueuse/core'; | |||||||
| import hljs from 'highlight.js/lib/core'; | import hljs from 'highlight.js/lib/core'; | ||||||
| import jsonHljs from 'highlight.js/lib/languages/json'; | import jsonHljs from 'highlight.js/lib/languages/json'; | ||||||
| import sqlHljs from 'highlight.js/lib/languages/sql'; | import sqlHljs from 'highlight.js/lib/languages/sql'; | ||||||
|  | import xmlHljs from 'highlight.js/lib/languages/xml'; | ||||||
| import { ref, toRefs } from 'vue'; | import { ref, toRefs } from 'vue'; | ||||||
|  |  | ||||||
| hljs.registerLanguage('sql', sqlHljs); | hljs.registerLanguage('sql', sqlHljs); | ||||||
| hljs.registerLanguage('json', jsonHljs); | hljs.registerLanguage('json', jsonHljs); | ||||||
|  | hljs.registerLanguage('html', xmlHljs); | ||||||
|  |  | ||||||
| const props = withDefaults( | const props = withDefaults( | ||||||
|   defineProps<{ |   defineProps<{ | ||||||
|   | |||||||
| @@ -49,7 +49,8 @@ a { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   .icon { |   .icon { | ||||||
|     opacity: 0.7; |     opacity: 0.6; | ||||||
|  |     color: #ffffff; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .title { |   .title { | ||||||
| @@ -57,7 +58,8 @@ a { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   .description { |   .description { | ||||||
|     opacity: 0.7; |     opacity: 0.6; | ||||||
|  |     color: #ffffff; | ||||||
|     margin: 5px 0; |     margin: 5px 0; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -134,23 +134,24 @@ const menuOptions: MenuGroupOption[] = toolsByCategory.map((category) => ({ | |||||||
|  |  | ||||||
|         <search-bar /> |         <search-bar /> | ||||||
|  |  | ||||||
|  |         <navbar-buttons v-if="!styleStore.isSmallScreen" /> | ||||||
|  |  | ||||||
|         <n-tooltip trigger="hover"> |         <n-tooltip trigger="hover"> | ||||||
|           <template #trigger> |           <template #trigger> | ||||||
|             <n-button |             <n-button | ||||||
|  |               round | ||||||
|               type="primary" |               type="primary" | ||||||
|               tag="a" |               tag="a" | ||||||
|               href="https://github.com/sponsors/CorentinTh" |               href="https://github.com/sponsors/CorentinTh" | ||||||
|               rel="noopener" |               rel="noopener" | ||||||
|               target="_blank" |               target="_blank" | ||||||
|             > |             > | ||||||
|               <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-right: 5px" /> |               Buy me a coffee | ||||||
|               Sponsor |               <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" style="margin-left: 5px" /> | ||||||
|             </n-button> |             </n-button> | ||||||
|           </template> |           </template> | ||||||
|           ❤ Support IT Tools development ! |           ❤ Support IT Tools development ! | ||||||
|         </n-tooltip> |         </n-tooltip> | ||||||
|  |  | ||||||
|         <navbar-buttons v-if="!styleStore.isSmallScreen" /> |  | ||||||
|       </div> |       </div> | ||||||
|       <slot /> |       <slot /> | ||||||
|     </template> |     </template> | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { toolsWithCategory } from '@/tools'; | import { toolsWithCategory } from '@/tools'; | ||||||
|  | import { Heart } from '@vicons/tabler'; | ||||||
| import { useHead } from '@vueuse/head'; | import { useHead } from '@vueuse/head'; | ||||||
|  | import ColoredCard from '../components/ColoredCard.vue'; | ||||||
| import ToolCard from '../components/ToolCard.vue'; | import ToolCard from '../components/ToolCard.vue'; | ||||||
|  |  | ||||||
| useHead({ title: 'IT Tools - Handy online tools for developers' }); | useHead({ title: 'IT Tools - Handy online tools for developers' }); | ||||||
| @@ -9,6 +11,27 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); | |||||||
| <template> | <template> | ||||||
|   <div class="home-page"> |   <div class="home-page"> | ||||||
|     <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> |     <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||||
|  |       <n-gi> | ||||||
|  |         <colored-card title="You like it-tools?" :icon="Heart"> | ||||||
|  |           Give us a star on | ||||||
|  |           <a | ||||||
|  |             href="https://github.com/CorentinTh/it-tools" | ||||||
|  |             rel="noopener" | ||||||
|  |             target="_blank" | ||||||
|  |             aria-label="IT-Tools' github repository" | ||||||
|  |             >github</a | ||||||
|  |           > | ||||||
|  |           or follow us on | ||||||
|  |           <a | ||||||
|  |             href="https://twitter.com/ittoolsdottech" | ||||||
|  |             rel="noopener" | ||||||
|  |             target="_blank" | ||||||
|  |             aria-label="IT-Tools' twitter account" | ||||||
|  |             >twitter</a | ||||||
|  |           >! Thank you | ||||||
|  |           <n-icon :component="Heart" /> | ||||||
|  |         </colored-card> | ||||||
|  |       </n-gi> | ||||||
|       <n-gi v-for="tool in toolsWithCategory" :key="tool.name"> |       <n-gi v-for="tool in toolsWithCategory" :key="tool.name"> | ||||||
|         <tool-card :tool="tool" /> |         <tool-card :tool="tool" /> | ||||||
|       </n-gi> |       </n-gi> | ||||||
|   | |||||||
| @@ -1,60 +1,64 @@ | |||||||
| import { | import { | ||||||
|   create, |   create, | ||||||
|  |   NAlert, | ||||||
|  |   NAutoComplete, | ||||||
|   NButton, |   NButton, | ||||||
|   NConfigProvider, |  | ||||||
|   NCard, |   NCard, | ||||||
|   NInput, |   NCode, | ||||||
|   NColorPicker, |  | ||||||
|   NInputNumber, |  | ||||||
|   NSpace, |  | ||||||
|   NH1, |  | ||||||
|   NForm, |  | ||||||
|   NFormItem, |  | ||||||
|   NTimePicker, |  | ||||||
|   NText, |  | ||||||
|   NIcon, |  | ||||||
|   NSwitch, |  | ||||||
|   NCollapseTransition, |  | ||||||
|   NGrid, |  | ||||||
|   NGridItem, |  | ||||||
|   NPopconfirm, |  | ||||||
|   NSlider, |  | ||||||
|   NCollapse, |   NCollapse, | ||||||
|   NCollapseItem, |   NCollapseItem, | ||||||
|   NProgress, |   NCollapseTransition, | ||||||
|   NAutoComplete, |   NColorPicker, | ||||||
|   NSelect, |   NConfigProvider, | ||||||
|   NUpload, |   NDatePicker, | ||||||
|   NEmpty, |   NDivider, | ||||||
|   NModal, |  | ||||||
|   NTooltip, |  | ||||||
|   NAlert, |  | ||||||
|   NP, |  | ||||||
|   NH2, |  | ||||||
|   NDropdown, |   NDropdown, | ||||||
|  |   NDynamicInput, | ||||||
|  |   NEllipsis, | ||||||
|  |   NEmpty, | ||||||
|  |   NForm, | ||||||
|  |   NFormItem, | ||||||
|  |   NGradientText, | ||||||
|  |   NGrid, | ||||||
|  |   NGridItem, | ||||||
|  |   NH1, | ||||||
|  |   NH2, | ||||||
|  |   NH3, | ||||||
|  |   NIcon, | ||||||
|  |   NImage, | ||||||
|  |   NInput, | ||||||
|  |   NInputGroup, | ||||||
|  |   NInputGroupLabel, | ||||||
|  |   NInputNumber, | ||||||
|   NLayout, |   NLayout, | ||||||
|   NLayoutSider, |   NLayoutSider, | ||||||
|   NMenu, |   NMenu, | ||||||
|   NMessageProvider, |   NMessageProvider, | ||||||
|  |   NModal, | ||||||
|  |   NP, | ||||||
|   NPageHeader, |   NPageHeader, | ||||||
|  |   NPopconfirm, | ||||||
|  |   NProgress, | ||||||
|   NResult, |   NResult, | ||||||
|   NH3, |  | ||||||
|   NEllipsis, |  | ||||||
|   NTag, |  | ||||||
|   NInputGroup, |  | ||||||
|   NInputGroupLabel, |  | ||||||
|   NDivider, |  | ||||||
|   NStatistic, |  | ||||||
|   NTable, |  | ||||||
|   NUploadDragger, |  | ||||||
|   NImage, |  | ||||||
|   NScrollbar, |   NScrollbar, | ||||||
|   NGradientText, |   NSelect, | ||||||
|   NCode, |   NSlider, | ||||||
|   NDatePicker, |   NSpace, | ||||||
|  |   NStatistic, | ||||||
|  |   NSwitch, | ||||||
|  |   NTable, | ||||||
|  |   NTag, | ||||||
|  |   NText, | ||||||
|  |   NTimePicker, | ||||||
|  |   NTooltip, | ||||||
|  |   NUpload, | ||||||
|  |   NUploadDragger, | ||||||
|  |   NCheckbox, | ||||||
| } from 'naive-ui'; | } from 'naive-ui'; | ||||||
|  |  | ||||||
| const components = [ | const components = [ | ||||||
|  |   NCheckbox, | ||||||
|  |   NDynamicInput, | ||||||
|   NDatePicker, |   NDatePicker, | ||||||
|   NCode, |   NCode, | ||||||
|   NGradientText, |   NGradientText, | ||||||
|   | |||||||
| @@ -30,4 +30,9 @@ export const darkThemeOverrides: GlobalThemeOverrides = { | |||||||
|     color: '#1e1e1e', |     color: '#1e1e1e', | ||||||
|     borderColor: 'transparent', |     borderColor: 'transparent', | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   Table: { | ||||||
|  |     tdColor: '#1e1e1e', | ||||||
|  |     thColor: '#353535', | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										68
									
								
								src/tools/chmod-calculator/chmod-calculator.service.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/tools/chmod-calculator/chmod-calculator.service.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | |||||||
|  | import { expect, describe, it } from 'vitest'; | ||||||
|  | import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | ||||||
|  |  | ||||||
|  | describe('chmod-calculator', () => { | ||||||
|  |   describe('computeChmodOctalRepresentation', () => { | ||||||
|  |     it('get the octal representation from permissions', () => { | ||||||
|  |       expect( | ||||||
|  |         computeChmodOctalRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: true, write: true, execute: true }, | ||||||
|  |             group: { read: true, write: true, execute: true }, | ||||||
|  |             public: { read: true, write: true, execute: true }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('777'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodOctalRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: false, write: false, execute: false }, | ||||||
|  |             group: { read: false, write: false, execute: false }, | ||||||
|  |             public: { read: false, write: false, execute: false }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('000'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodOctalRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: false, write: true, execute: false }, | ||||||
|  |             group: { read: false, write: true, execute: true }, | ||||||
|  |             public: { read: true, write: false, execute: true }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('235'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodOctalRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: true, write: false, execute: false }, | ||||||
|  |             group: { read: false, write: true, execute: false }, | ||||||
|  |             public: { read: false, write: false, execute: true }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('421'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodOctalRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: false, write: false, execute: true }, | ||||||
|  |             group: { read: false, write: true, execute: false }, | ||||||
|  |             public: { read: true, write: false, execute: false }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('124'); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         computeChmodOctalRepresentation({ | ||||||
|  |           permissions: { | ||||||
|  |             owner: { read: false, write: true, execute: false }, | ||||||
|  |             group: { read: false, write: true, execute: false }, | ||||||
|  |             public: { read: false, write: true, execute: false }, | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |       ).to.eql('222'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										17
									
								
								src/tools/chmod-calculator/chmod-calculator.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/tools/chmod-calculator/chmod-calculator.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | import _ from 'lodash'; | ||||||
|  | import type { GroupPermissions, Permissions } from './chmod-calculator.types'; | ||||||
|  |  | ||||||
|  | export { computeChmodOctalRepresentation }; | ||||||
|  |  | ||||||
|  | function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string { | ||||||
|  |   const permissionValue = { read: 4, write: 2, execute: 1 }; | ||||||
|  |  | ||||||
|  |   const getGroupPermissionValue = (permission: GroupPermissions) => | ||||||
|  |     _.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, 0) : 0), 0); | ||||||
|  |  | ||||||
|  |   return [ | ||||||
|  |     getGroupPermissionValue(permissions.owner), | ||||||
|  |     getGroupPermissionValue(permissions.group), | ||||||
|  |     getGroupPermissionValue(permissions.public), | ||||||
|  |   ].join(''); | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								src/tools/chmod-calculator/chmod-calculator.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/tools/chmod-calculator/chmod-calculator.types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | export type Scope = 'read' | 'write' | 'execute'; | ||||||
|  | export type Group = 'owner' | 'group' | 'public'; | ||||||
|  |  | ||||||
|  | export type GroupPermissions = { | ||||||
|  |   [k in Scope]: boolean; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type Permissions = { | ||||||
|  |   [k in Group]: GroupPermissions; | ||||||
|  | }; | ||||||
							
								
								
									
										83
									
								
								src/tools/chmod-calculator/chmod-calculator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/tools/chmod-calculator/chmod-calculator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table"> | ||||||
|  |       <thead> | ||||||
|  |         <tr> | ||||||
|  |           <th class="text-center" scope="col"></th> | ||||||
|  |           <th class="text-center" scope="col">Owner (u)</th> | ||||||
|  |           <th class="text-center" scope="col">Group (g)</th> | ||||||
|  |           <th class="text-center" scope="col">Public (o)</th> | ||||||
|  |         </tr> | ||||||
|  |       </thead> | ||||||
|  |       <tbody> | ||||||
|  |         <tr v-for="{ scope, title } of scopes" :key="scope"> | ||||||
|  |           <td class="line-header">{{ title }}</td> | ||||||
|  |           <td v-for="group of groups" :key="group" class="text-center"> | ||||||
|  |             <!-- <n-switch v-model:value="permissions[group][scope]" /> --> | ||||||
|  |             <n-checkbox v-model:checked="permissions[group][scope]" size="large" /> | ||||||
|  |           </td> | ||||||
|  |         </tr> | ||||||
|  |       </tbody> | ||||||
|  |     </n-table> | ||||||
|  |  | ||||||
|  |     <div class="octal-result"> | ||||||
|  |       {{ octal }} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <input-copyable :value="`chmod ${octal} path`" readonly style="margin-bottom: 5px" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useThemeVars } from 'naive-ui'; | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | ||||||
|  | import InputCopyable from '../../components/InputCopyable.vue'; | ||||||
|  |  | ||||||
|  | import type { Group, Scope } from './chmod-calculator.types'; | ||||||
|  |  | ||||||
|  | const themeVars = useThemeVars(); | ||||||
|  |  | ||||||
|  | const scopes: { scope: Scope; title: string }[] = [ | ||||||
|  |   { scope: 'read', title: 'Read (4)' }, | ||||||
|  |   { scope: 'write', title: 'Write (2)' }, | ||||||
|  |   { scope: 'execute', title: 'Execute (1)' }, | ||||||
|  | ]; | ||||||
|  | const groups: Group[] = ['owner', 'group', 'public']; | ||||||
|  |  | ||||||
|  | const permissions = ref({ | ||||||
|  |   owner: { read: false, write: false, execute: false }, | ||||||
|  |   group: { read: false, write: false, execute: false }, | ||||||
|  |   public: { read: false, write: false, execute: false }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="less" scoped> | ||||||
|  | .octal-result { | ||||||
|  |   text-align: center; | ||||||
|  |   font-size: 50px; | ||||||
|  |   font-family: monospace; | ||||||
|  |   color: v-bind('themeVars.primaryColor'); | ||||||
|  |   margin: 20px 0; | ||||||
|  | } | ||||||
|  | .permission-table { | ||||||
|  |   td, | ||||||
|  |   th { | ||||||
|  |     padding: 15px; | ||||||
|  |  | ||||||
|  |     @media screen and (max-width: 600px) { | ||||||
|  |       padding: 5px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .line-header { | ||||||
|  |   font-weight: bold; | ||||||
|  |   text-align: right; | ||||||
|  |   max-width: 80px; | ||||||
|  | } | ||||||
|  | .text-center { | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										22
									
								
								src/tools/chmod-calculator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/tools/chmod-calculator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import { FileInvoice } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  |  | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'Chmod calculator', | ||||||
|  |   path: '/chmod-calculator', | ||||||
|  |   description: 'Compute your chmod permissions and commands with this online chmod calculator.', | ||||||
|  |   keywords: [ | ||||||
|  |     'chmod', | ||||||
|  |     'calculator', | ||||||
|  |     'file', | ||||||
|  |     'permission', | ||||||
|  |     'files', | ||||||
|  |     'directory', | ||||||
|  |     'folder', | ||||||
|  |     'recursive', | ||||||
|  |     'generator', | ||||||
|  |     'octal', | ||||||
|  |   ], | ||||||
|  |   component: () => import('./chmod-calculator.vue'), | ||||||
|  |   icon: FileInvoice, | ||||||
|  | }); | ||||||
| @@ -132,5 +132,10 @@ const formats: Format[] = [ | |||||||
|     fromDate: (date) => date.toUTCString(), |     fromDate: (date) => date.toUTCString(), | ||||||
|     toDate, |     toDate, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     name: 'Mongo ObjectID', | ||||||
|  |     fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000', | ||||||
|  |     toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000), | ||||||
|  |   }, | ||||||
| ]; | ]; | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
| import { LockOpen } from '@vicons/tabler'; | import { LockOpen } from '@vicons/tabler'; | ||||||
| import type { ToolCategory } from './tool'; | import type { ToolCategory } from './tool'; | ||||||
|  |  | ||||||
|  | import { tool as chmodCalculator } from './chmod-calculator'; | ||||||
|  | import { tool as mimeTypes } from './mime-types'; | ||||||
|  | import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; | ||||||
| import { tool as base64FileConverter } from './base64-file-converter'; | import { tool as base64FileConverter } from './base64-file-converter'; | ||||||
| import { tool as base64StringConverter } from './base64-string-converter'; | import { tool as base64StringConverter } from './base64-string-converter'; | ||||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||||
| @@ -22,6 +25,7 @@ import { tool as baseConverter } from './integer-base-converter'; | |||||||
| import { tool as jsonViewer } from './json-viewer'; | import { tool as jsonViewer } from './json-viewer'; | ||||||
| import { tool as loremIpsumGenerator } from './lorem-ipsum-generator'; | import { tool as loremIpsumGenerator } from './lorem-ipsum-generator'; | ||||||
| import { tool as mathEvaluator } from './math-evaluator'; | import { tool as mathEvaluator } from './math-evaluator'; | ||||||
|  | import { tool as metaTagGenerator } from './meta-tag-generator'; | ||||||
| import { tool as qrCodeGenerator } from './qr-code-generator'; | import { tool as qrCodeGenerator } from './qr-code-generator'; | ||||||
| import { tool as randomPortGenerator } from './random-port-generator'; | import { tool as randomPortGenerator } from './random-port-generator'; | ||||||
| import { tool as romanNumeralConverter } from './roman-numeral-converter'; | import { tool as romanNumeralConverter } from './roman-numeral-converter'; | ||||||
| @@ -55,7 +59,16 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|   { |   { | ||||||
|     name: 'Web', |     name: 'Web', | ||||||
|     icon: LockOpen, |     icon: LockOpen, | ||||||
|     components: [urlEncoder, htmlEntities, urlParser, deviceInformation, basicAuthGenerator], |     components: [ | ||||||
|  |       urlEncoder, | ||||||
|  |       htmlEntities, | ||||||
|  |       urlParser, | ||||||
|  |       deviceInformation, | ||||||
|  |       basicAuthGenerator, | ||||||
|  |       metaTagGenerator, | ||||||
|  |       otpCodeGeneratorAndValidator, | ||||||
|  |       mimeTypes, | ||||||
|  |     ], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Images', |     name: 'Images', | ||||||
| @@ -65,7 +78,7 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|   { |   { | ||||||
|     name: 'Development', |     name: 'Development', | ||||||
|     icon: LockOpen, |     icon: LockOpen, | ||||||
|     components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify], |     components: [gitMemo, randomPortGenerator, crontabGenerator, jsonViewer, sqlPrettify, chmodCalculator], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Math', |     name: 'Math', | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								src/tools/meta-tag-generator/OGSchemaType.type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/tools/meta-tag-generator/OGSchemaType.type.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | import type { SelectGroupOption, SelectOption } from 'naive-ui'; | ||||||
|  |  | ||||||
|  | export type { OGSchemaType, OGSchemaTypeElementInput, OGSchemaTypeElementSelect, OGSchemaTypeElementInputMultiple }; | ||||||
|  |  | ||||||
|  | interface OGSchemaTypeElementBase { | ||||||
|  |   key: string; | ||||||
|  |   label: string; | ||||||
|  |   placeholder: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface OGSchemaTypeElementInput extends OGSchemaTypeElementBase { | ||||||
|  |   type: 'input'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface OGSchemaTypeElementInputMultiple extends OGSchemaTypeElementBase { | ||||||
|  |   type: 'input-multiple'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface OGSchemaTypeElementSelect extends OGSchemaTypeElementBase { | ||||||
|  |   type: 'select'; | ||||||
|  |   options: Array<SelectOption | SelectGroupOption>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface OGSchemaType { | ||||||
|  |   name: string; | ||||||
|  |   elements: (OGSchemaTypeElementSelect | OGSchemaTypeElementInput | OGSchemaTypeElementInputMultiple)[]; | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								src/tools/meta-tag-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/tools/meta-tag-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import { Tags } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  |  | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'Open graph meta generator', | ||||||
|  |   path: '/og-meta-generator', | ||||||
|  |   description: 'Generate open-graph and socials html meta tags for your website.', | ||||||
|  |   keywords: [ | ||||||
|  |     'meta', | ||||||
|  |     'tag', | ||||||
|  |     'generator', | ||||||
|  |     'social', | ||||||
|  |     'title', | ||||||
|  |     'description', | ||||||
|  |     'image', | ||||||
|  |     'share', | ||||||
|  |     'online', | ||||||
|  |     'website', | ||||||
|  |     'open', | ||||||
|  |     'graph', | ||||||
|  |     'og', | ||||||
|  |   ], | ||||||
|  |   component: () => import('./meta-tag-generator.vue'), | ||||||
|  |   icon: Tags, | ||||||
|  | }); | ||||||
							
								
								
									
										94
									
								
								src/tools/meta-tag-generator/meta-tag-generator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/tools/meta-tag-generator/meta-tag-generator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <div v-for="{ name, elements } of sections" :key="name" style="margin-bottom: 15px"> | ||||||
|  |       <n-form-item :label="name" :show-feedback="false"> </n-form-item> | ||||||
|  |  | ||||||
|  |       <n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key"> | ||||||
|  |         <n-input-group-label style="flex: 0 0 110px">{{ label }}</n-input-group-label> | ||||||
|  |         <n-input v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" /> | ||||||
|  |         <n-dynamic-input | ||||||
|  |           v-else-if="type === 'input-multiple'" | ||||||
|  |           v-model:value="metadata[key]" | ||||||
|  |           :min="1" | ||||||
|  |           :placeholder="placeholder" | ||||||
|  |           :default-value="['']" | ||||||
|  |           :show-sort-button="true" | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <n-select | ||||||
|  |           v-else-if="type === 'select'" | ||||||
|  |           v-model:value="metadata[key]" | ||||||
|  |           :placeholder="placeholder" | ||||||
|  |           :options="(element as OGSchemaTypeElementSelect).options" | ||||||
|  |         /> | ||||||
|  |       </n-input-group> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |   <div> | ||||||
|  |     <n-form-item label="Your meta tags"> | ||||||
|  |       <textarea-copyable :value="metaTags" language="html" /> | ||||||
|  |     </n-form-item> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||||
|  | import { generateMeta } from '@it-tools/oggen'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import { computed, ref, watch } from 'vue'; | ||||||
|  | import { image, ogSchemas, twitter, website } from './og-schemas'; | ||||||
|  | import type { OGSchemaType, OGSchemaTypeElementSelect } from './OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | // Since type guards do not work in template | ||||||
|  | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
|  | const metadata = ref<{ type: string; [k: string]: any }>({ | ||||||
|  |   type: 'website', | ||||||
|  |   'twitter:card': 'summary_large_image', | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | watch( | ||||||
|  |   () => ref(metadata.value.type), | ||||||
|  |   (_ignored, prevSection) => { | ||||||
|  |     const section = ogSchemas[prevSection.value]; | ||||||
|  |  | ||||||
|  |     if (!section) return; | ||||||
|  |  | ||||||
|  |     section.elements.forEach(({ key }) => { | ||||||
|  |       metadata.value[key] = ''; | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const sections = computed(() => { | ||||||
|  |   const secs: OGSchemaType[] = [website, image, twitter]; | ||||||
|  |   const additionalSchema = ogSchemas[metadata.value.type]; | ||||||
|  |  | ||||||
|  |   if (additionalSchema) secs.push(additionalSchema); | ||||||
|  |  | ||||||
|  |   return secs; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const metaTags = computed(() => { | ||||||
|  |   const twitterMeta = _.chain(metadata.value) | ||||||
|  |     .pickBy((_value, k) => k.startsWith('twitter:')) | ||||||
|  |     .mapKeys((_value, k) => k.replace(/^twitter:/, '')) | ||||||
|  |     .value(); | ||||||
|  |  | ||||||
|  |   const otherMeta = _.pickBy(metadata.value, (_value, k) => !k.startsWith('twitter:')); | ||||||
|  |  | ||||||
|  |   return generateMeta({ ...otherMeta, twitter: twitterMeta }, { generateTwitterCompatibleMeta: true }); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="less" scoped> | ||||||
|  | .n-input-group { | ||||||
|  |   margin-bottom: 5px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ::v-deep(.n-form-item-blank) { | ||||||
|  |   min-height: 0 !important; | ||||||
|  | } | ||||||
|  | ::v-deep(.n-dynamic-input-item) { | ||||||
|  |   margin-bottom: 5px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										33
									
								
								src/tools/meta-tag-generator/og-schemas/article.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/tools/meta-tag-generator/og-schemas/article.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const article: OGSchemaType = { | ||||||
|  |   name: 'Article', | ||||||
|  |   elements: [ | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Publishing date', | ||||||
|  |       key: 'article:published_time', | ||||||
|  |       placeholder: 'When the article was first published...', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Modification date', | ||||||
|  |       key: 'article:modified_time', | ||||||
|  |       placeholder: 'When the article was last changed...', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Expiration date', | ||||||
|  |       key: 'article:expiration_time', | ||||||
|  |       placeholder: 'When the article is out of date after...', | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Author', key: 'article:author', placeholder: 'Writers of the article...' }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Section', | ||||||
|  |       key: 'article:section', | ||||||
|  |       placeholder: 'A high-level section name. E.g. Technology..', | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Tag', key: 'article:tag', placeholder: 'Tag words associated with this article...' }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										16
									
								
								src/tools/meta-tag-generator/og-schemas/book.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/tools/meta-tag-generator/og-schemas/book.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const book: OGSchemaType = { | ||||||
|  |   name: 'Book', | ||||||
|  |   elements: [ | ||||||
|  |     { type: 'input', label: 'Author', key: 'book:author', placeholder: 'Who wrote this book...' }, | ||||||
|  |     { type: 'input', label: 'ISBN', key: 'book:isbn', placeholder: 'The International Standard Book Number...' }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Release date', | ||||||
|  |       key: 'book:release_date', | ||||||
|  |       placeholder: 'The date the book was released...', | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Tag', key: 'book:tag', placeholder: 'Tag words associated with this book...' }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										31
									
								
								src/tools/meta-tag-generator/og-schemas/image.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/tools/meta-tag-generator/og-schemas/image.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const image: OGSchemaType = { | ||||||
|  |   name: 'Image', | ||||||
|  |   elements: [ | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Image url', | ||||||
|  |       placeholder: 'The url of your website social image...', | ||||||
|  |       key: 'image', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Image alt', | ||||||
|  |       placeholder: 'The alternative text of your website social image...', | ||||||
|  |       key: 'image:alt', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Width', | ||||||
|  |       placeholder: 'Width in px of your website social image...', | ||||||
|  |       key: 'image:width', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Height', | ||||||
|  |       placeholder: 'Height in px of your website social image...', | ||||||
|  |       key: 'image:height', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										31
									
								
								src/tools/meta-tag-generator/og-schemas/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/tools/meta-tag-generator/og-schemas/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | import { article } from './article'; | ||||||
|  | import { book } from './book'; | ||||||
|  | import { musicAlbum } from './musicAlbum'; | ||||||
|  | import { musicPlaylist } from './musicPlaylist'; | ||||||
|  | import { musicRadioStation } from './musicRadioStation'; | ||||||
|  | import { musicSong } from './musicSong'; | ||||||
|  | import { profile } from './profile'; | ||||||
|  | import { videoEpisode } from './videoEpisode'; | ||||||
|  | import { videoMovie } from './videoMovie'; | ||||||
|  | import { videoOther } from './videoOther'; | ||||||
|  | import { videoTVShow } from './videoTVShow'; | ||||||
|  |  | ||||||
|  | export * from './image'; | ||||||
|  | export * from './twitter'; | ||||||
|  | export * from './website'; | ||||||
|  |  | ||||||
|  | export const ogSchemas: Record<string, OGSchemaType> = { | ||||||
|  |   'music.song': musicSong, | ||||||
|  |   'music.album': musicAlbum, | ||||||
|  |   'music.playlist': musicPlaylist, | ||||||
|  |   'music.radio_station': musicRadioStation, | ||||||
|  |   'video.movie': videoMovie, | ||||||
|  |   'video.episode': videoEpisode, | ||||||
|  |   'video.tv_show': videoTVShow, | ||||||
|  |   'video.other': videoOther, | ||||||
|  |   profile, | ||||||
|  |   article, | ||||||
|  |   book, | ||||||
|  | }; | ||||||
							
								
								
									
										27
									
								
								src/tools/meta-tag-generator/og-schemas/musicAlbum.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/tools/meta-tag-generator/og-schemas/musicAlbum.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const musicAlbum: OGSchemaType = { | ||||||
|  |   name: 'Album details', | ||||||
|  |   elements: [ | ||||||
|  |     { type: 'input', label: 'Song', key: 'music:song', placeholder: 'The song on this album...' }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Disc', | ||||||
|  |       key: 'music:song:disc', | ||||||
|  |       placeholder: 'The same as music:album:disc but in reverse...', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Track', | ||||||
|  |       key: 'music:song:track', | ||||||
|  |       placeholder: 'The same as music:album:track but in reverse...', | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Musician', key: 'music:musician', placeholder: 'The musician that made this song...' }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Release date', | ||||||
|  |       key: 'music:release_date', | ||||||
|  |       placeholder: 'The date the album was released...', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										21
									
								
								src/tools/meta-tag-generator/og-schemas/musicPlaylist.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/tools/meta-tag-generator/og-schemas/musicPlaylist.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const musicPlaylist: OGSchemaType = { | ||||||
|  |   name: 'Playlist details', | ||||||
|  |   elements: [ | ||||||
|  |     { type: 'input', label: 'Song', key: 'music:song', placeholder: 'The song on this album...' }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Disc', | ||||||
|  |       key: 'music:song:disc', | ||||||
|  |       placeholder: 'The same as music:album:disc but in reverse...', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Track', | ||||||
|  |       key: 'music:song:track', | ||||||
|  |       placeholder: 'The same as music:album:track but in reverse...', | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Creator', key: 'music:creator', placeholder: 'The creator of this playlist...' }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const musicRadioStation: OGSchemaType = { | ||||||
|  |   name: 'Radio station details', | ||||||
|  |   elements: [ | ||||||
|  |     { type: 'input', label: 'Creator', key: 'music:creator', placeholder: 'The creator of this radio station...' }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										22
									
								
								src/tools/meta-tag-generator/og-schemas/musicSong.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/tools/meta-tag-generator/og-schemas/musicSong.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const musicSong: OGSchemaType = { | ||||||
|  |   name: 'Song details', | ||||||
|  |   elements: [ | ||||||
|  |     { type: 'input', label: 'Duration', placeholder: 'The duration of the song...', key: 'music:duration' }, | ||||||
|  |     { type: 'input', label: 'Album', placeholder: 'The album this song is from...', key: 'music:album' }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Disc', | ||||||
|  |       placeholder: 'Which disc of the album this song is on...', | ||||||
|  |       key: 'music:album:disk', | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Track', placeholder: ' Which track this song is...', key: 'music:album:track' }, | ||||||
|  |     { | ||||||
|  |       type: 'input-multiple', | ||||||
|  |       label: 'Musician', | ||||||
|  |       placeholder: 'The musician that made this song...', | ||||||
|  |       key: 'music:musician', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										21
									
								
								src/tools/meta-tag-generator/og-schemas/profile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/tools/meta-tag-generator/og-schemas/profile.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const profile: OGSchemaType = { | ||||||
|  |   name: 'Profile', | ||||||
|  |   elements: [ | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'First name', | ||||||
|  |       placeholder: 'Enter the first name of the person...', | ||||||
|  |       key: 'profile:first_name', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Last name', | ||||||
|  |       placeholder: 'Enter the last name of the person...', | ||||||
|  |       key: 'profile:last_name', | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Username', placeholder: 'Enter the username of the person...', key: 'profile:username' }, | ||||||
|  |     { type: 'input', label: 'Gender', placeholder: 'Enter the gender of the person...', key: 'profile:gender' }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										31
									
								
								src/tools/meta-tag-generator/og-schemas/twitter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/tools/meta-tag-generator/og-schemas/twitter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const twitter: OGSchemaType = { | ||||||
|  |   name: 'Twitter', | ||||||
|  |   elements: [ | ||||||
|  |     { | ||||||
|  |       type: 'select', | ||||||
|  |       options: [ | ||||||
|  |         { label: 'Summary', value: 'summary' }, | ||||||
|  |         { label: 'Summary with large image', value: 'summary_large_image' }, | ||||||
|  |         { label: 'Application', value: 'app' }, | ||||||
|  |         { label: 'Player', value: 'player' }, | ||||||
|  |       ], | ||||||
|  |       label: 'Card type', | ||||||
|  |       placeholder: 'The twitter card type...', | ||||||
|  |       key: 'twitter:card', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Site account', | ||||||
|  |       placeholder: 'The name of the twitter account of the site (ex: @ittoolsdottech)...', | ||||||
|  |       key: 'twitter:site', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Creator acc.', | ||||||
|  |       placeholder: 'The name of the twitter account of the creator (ex: @cthmsst)...', | ||||||
|  |       key: 'twitter:creator', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										10
									
								
								src/tools/meta-tag-generator/og-schemas/videoEpisode.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/tools/meta-tag-generator/og-schemas/videoEpisode.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  | import { videoMovie } from './videoMovie'; | ||||||
|  |  | ||||||
|  | export const videoEpisode: OGSchemaType = { | ||||||
|  |   name: 'Video episode details', | ||||||
|  |   elements: [ | ||||||
|  |     ...videoMovie.elements, | ||||||
|  |     { type: 'input', label: 'Series', key: 'video:series', placeholder: 'Which series this episode belongs to...' }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										29
									
								
								src/tools/meta-tag-generator/og-schemas/videoMovie.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/tools/meta-tag-generator/og-schemas/videoMovie.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | export const videoMovie: OGSchemaType = { | ||||||
|  |   name: 'Movie details', | ||||||
|  |   elements: [ | ||||||
|  |     { | ||||||
|  |       type: 'input-multiple', | ||||||
|  |       label: 'Actor', | ||||||
|  |       key: 'video:actor', | ||||||
|  |       placeholder: 'Name of the actress/actor...', | ||||||
|  |     }, | ||||||
|  |     // { type: 'input', label: 'Actor role', key: 'video:actor:role', placeholder: 'The role they played...' }, | ||||||
|  |     { | ||||||
|  |       type: 'input-multiple', | ||||||
|  |       label: 'Director', | ||||||
|  |       key: 'video:director', | ||||||
|  |       placeholder: 'Name of the director...', | ||||||
|  |     }, | ||||||
|  |     { type: 'input-multiple', label: 'Writer', key: 'video:writer', placeholder: 'Writers of the movie...' }, | ||||||
|  |     { type: 'input', label: 'Duration', key: 'video:duration', placeholder: "The movie's length in seconds..." }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Release date', | ||||||
|  |       key: 'video:release_date', | ||||||
|  |       placeholder: 'The date the movie was released...', | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Tag', key: 'video:tag', placeholder: 'Tag words associated with this movie...' }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										7
									
								
								src/tools/meta-tag-generator/og-schemas/videoOther.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/tools/meta-tag-generator/og-schemas/videoOther.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  | import { videoMovie } from './videoMovie'; | ||||||
|  |  | ||||||
|  | export const videoOther: OGSchemaType = { | ||||||
|  |   name: 'Other video details', | ||||||
|  |   elements: [...videoMovie.elements], | ||||||
|  | }; | ||||||
							
								
								
									
										7
									
								
								src/tools/meta-tag-generator/og-schemas/videoTVShow.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/tools/meta-tag-generator/og-schemas/videoTVShow.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  | import { videoMovie } from './videoMovie'; | ||||||
|  |  | ||||||
|  | export const videoTVShow: OGSchemaType = { | ||||||
|  |   name: 'TV show details', | ||||||
|  |   elements: [...videoMovie.elements], | ||||||
|  | }; | ||||||
							
								
								
									
										56
									
								
								src/tools/meta-tag-generator/og-schemas/website.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/tools/meta-tag-generator/og-schemas/website.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | import type { OGSchemaType } from '../OGSchemaType.type'; | ||||||
|  |  | ||||||
|  | const typeOptions = [ | ||||||
|  |   { label: 'Website', value: 'website' }, | ||||||
|  |   { label: 'Article', value: 'article' }, | ||||||
|  |   { label: 'Book', value: 'book' }, | ||||||
|  |   { label: 'Profile', value: 'profile' }, | ||||||
|  |   { | ||||||
|  |     type: 'group', | ||||||
|  |     label: 'Music', | ||||||
|  |     key: 'Music', | ||||||
|  |     children: [ | ||||||
|  |       { label: 'Song', value: 'music.song' }, | ||||||
|  |       { label: 'Music album', value: 'music.album' }, | ||||||
|  |       { label: 'Playlist', value: 'music.playlist' }, | ||||||
|  |       { label: 'Radio station', value: 'music.radio_station' }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     type: 'group', | ||||||
|  |     label: 'Video', | ||||||
|  |     key: 'Video', | ||||||
|  |     children: [ | ||||||
|  |       { label: 'Movie', value: 'video.movie' }, | ||||||
|  |       { label: 'Episode', value: 'video.episode' }, | ||||||
|  |       { label: 'TV show', value: 'video.tv_show' }, | ||||||
|  |       { label: 'Other video', value: 'video.other' }, | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const website: OGSchemaType = { | ||||||
|  |   name: 'General information', | ||||||
|  |   elements: [ | ||||||
|  |     { | ||||||
|  |       type: 'select', | ||||||
|  |       label: 'Page type', | ||||||
|  |       placeholder: 'Select the type of your website...', | ||||||
|  |       key: 'type', | ||||||
|  |       options: typeOptions, | ||||||
|  |     }, | ||||||
|  |     { type: 'input', label: 'Title', placeholder: 'Enter the title of your website...', key: 'title' }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Description', | ||||||
|  |       placeholder: 'Enter the description of your website...', | ||||||
|  |       key: 'description', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       type: 'input', | ||||||
|  |       label: 'Page URL', | ||||||
|  |       placeholder: 'Enter the url of your website...', | ||||||
|  |       key: 'url', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
							
								
								
									
										11
									
								
								src/tools/mime-types/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/tools/mime-types/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | import { World } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  |  | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'Mime types', | ||||||
|  |   path: '/mime-types', | ||||||
|  |   description: 'Convert mime types to extensions and vice-versa.', | ||||||
|  |   keywords: ['mime', 'types', 'extension', 'content', 'type'], | ||||||
|  |   component: () => import('./mime-types.vue'), | ||||||
|  |   icon: World, | ||||||
|  | }); | ||||||
							
								
								
									
										99
									
								
								src/tools/mime-types/mime-types.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/tools/mime-types/mime-types.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | <template> | ||||||
|  |   <n-card> | ||||||
|  |     <n-h2 style="margin-bottom: 0">Mime type to extension</n-h2> | ||||||
|  |     <div style="opacity: 0.8">Now witch file extensions are associated to a mime-type</div> | ||||||
|  |     <n-form-item> | ||||||
|  |       <n-select | ||||||
|  |         v-model:value="selectedMimeType" | ||||||
|  |         filterable | ||||||
|  |         :options="mimeToExtensionsOptions" | ||||||
|  |         size="large" | ||||||
|  |         placeholder="Select your mimetype here... (ex: application/pdf)" | ||||||
|  |       /> | ||||||
|  |     </n-form-item> | ||||||
|  |  | ||||||
|  |     <div v-if="extensionsFound.length > 0"> | ||||||
|  |       Extensions of files with the <n-tag round :bordered="false">{{ selectedMimeType }}</n-tag> mime-type: | ||||||
|  |       <div style="margin-top: 10px"> | ||||||
|  |         <n-tag | ||||||
|  |           v-for="extension of extensionsFound" | ||||||
|  |           :key="extension" | ||||||
|  |           round | ||||||
|  |           :bordered="false" | ||||||
|  |           type="primary" | ||||||
|  |           style="margin-right: 10px" | ||||||
|  |         > | ||||||
|  |           .{{ extension }} | ||||||
|  |         </n-tag> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </n-card> | ||||||
|  |  | ||||||
|  |   <n-card> | ||||||
|  |     <n-h2 style="margin-bottom: 0">File extension to mime type</n-h2> | ||||||
|  |     <div style="opacity: 0.8">Now witch mime type is associated to a file extension</div> | ||||||
|  |     <n-form-item> | ||||||
|  |       <n-select | ||||||
|  |         v-model:value="selectedExtension" | ||||||
|  |         filterable | ||||||
|  |         :options="extensionToMimeTypeOptions" | ||||||
|  |         size="large" | ||||||
|  |         placeholder="Select your mimetype here... (ex: application/pdf)" | ||||||
|  |       /> | ||||||
|  |     </n-form-item> | ||||||
|  |  | ||||||
|  |     <div v-if="selectedExtension"> | ||||||
|  |       Mime type associated to the extension <n-tag round :bordered="false">{{ selectedExtension }}</n-tag> file | ||||||
|  |       extension: | ||||||
|  |       <div style="margin-top: 10px"> | ||||||
|  |         <n-tag round :bordered="false" type="primary" style="margin-right: 10px"> | ||||||
|  |           {{ mimeTypeFound }} | ||||||
|  |         </n-tag> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </n-card> | ||||||
|  |  | ||||||
|  |   <div> | ||||||
|  |     <n-table> | ||||||
|  |       <thead> | ||||||
|  |         <tr> | ||||||
|  |           <th>Mime types</th> | ||||||
|  |           <th>Extensions</th> | ||||||
|  |         </tr> | ||||||
|  |       </thead> | ||||||
|  |       <tbody> | ||||||
|  |         <tr v-for="{ mimeType, extensions } of mimeInfos" :key="mimeType"> | ||||||
|  |           <td>{{ mimeType }}</td> | ||||||
|  |           <td> | ||||||
|  |             <n-tag v-for="extension of extensions" :key="extension" round :bordered="false" style="margin-right: 10px"> | ||||||
|  |               .{{ extension }} | ||||||
|  |             </n-tag> | ||||||
|  |           </td> | ||||||
|  |         </tr> | ||||||
|  |       </tbody> | ||||||
|  |     </n-table> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { types as extensionToMimeType, extensions as mimeTypeToExtension } from 'mime-types'; | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  |  | ||||||
|  | const mimeInfos = Object.entries(mimeTypeToExtension).map(([mimeType, extensions]) => ({ mimeType, extensions })); | ||||||
|  |  | ||||||
|  | const mimeToExtensionsOptions = Object.keys(mimeTypeToExtension).map((label) => ({ label, value: label })); | ||||||
|  | const selectedMimeType = ref(undefined); | ||||||
|  |  | ||||||
|  | const extensionsFound = computed(() => (selectedMimeType.value ? mimeTypeToExtension[selectedMimeType.value] : [])); | ||||||
|  |  | ||||||
|  | const extensionToMimeTypeOptions = Object.keys(extensionToMimeType).map((label) => { | ||||||
|  |   const extension = `.${label}`; | ||||||
|  |  | ||||||
|  |   return { label: extension, value: label }; | ||||||
|  | }); | ||||||
|  | const selectedExtension = ref(undefined); | ||||||
|  |  | ||||||
|  | const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeType[selectedExtension.value] : [])); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="less" scoped></style> | ||||||
							
								
								
									
										27
									
								
								src/tools/otp-code-generator-and-validator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/tools/otp-code-generator-and-validator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | import { DeviceMobile } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  |  | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'OTP code generator', | ||||||
|  |   path: '/otp-generator', | ||||||
|  |   description: 'Generate and validate time-based OTP (one time password) for multi-factor authentication.', | ||||||
|  |   keywords: [ | ||||||
|  |     'otp', | ||||||
|  |     'code', | ||||||
|  |     'generator', | ||||||
|  |     'validator', | ||||||
|  |     'one', | ||||||
|  |     'time', | ||||||
|  |     'password', | ||||||
|  |     'authentication', | ||||||
|  |     'MFA', | ||||||
|  |     'mobile', | ||||||
|  |     'device', | ||||||
|  |     'security', | ||||||
|  |     'TOTP', | ||||||
|  |     'Time', | ||||||
|  |     'HMAC', | ||||||
|  |   ], | ||||||
|  |   component: () => import('./otp-code-generator-and-validator.vue'), | ||||||
|  |   icon: DeviceMobile, | ||||||
|  | }); | ||||||
| @@ -0,0 +1,140 @@ | |||||||
|  | <template> | ||||||
|  |   <div style="max-width: 350px"> | ||||||
|  |     <n-form-item label="Secret" v-bind="secretValidationAttrs"> | ||||||
|  |       <n-input v-model:value="secret" placeholder="Paste your TOTP secret..."> | ||||||
|  |         <template #suffix> | ||||||
|  |           <n-tooltip trigger="hover"> | ||||||
|  |             <template #trigger> | ||||||
|  |               <n-button quaternary circle @click="refreshSecret"> | ||||||
|  |                 <n-icon :component="Refresh" /> | ||||||
|  |               </n-button> | ||||||
|  |             </template> | ||||||
|  |             Generate secret token | ||||||
|  |           </n-tooltip> | ||||||
|  |         </template> | ||||||
|  |       </n-input> | ||||||
|  |     </n-form-item> | ||||||
|  |  | ||||||
|  |     <div> | ||||||
|  |       <token-display :tokens="tokens" style="margin-top: 2px" /> | ||||||
|  |  | ||||||
|  |       <n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" /> | ||||||
|  |       <div style="text-align: center">Next in {{ String(Math.floor(30 - interval)).padStart(2, '0') }}s</div> | ||||||
|  |     </div> | ||||||
|  |     <n-space justify="center" vertical align="center" style="margin-top: 10px"> | ||||||
|  |       <n-image :src="qrcode"></n-image> | ||||||
|  |       <n-button secondary tag="a" :href="keyUri" target="_blank">Open Key URI in new tab</n-button> | ||||||
|  |     </n-space> | ||||||
|  |   </div> | ||||||
|  |   <div style="max-width: 350px"> | ||||||
|  |     <n-form-item label="Secret in hexadecimal"> | ||||||
|  |       <input-copyable :value="base32toHex(secret)" readonly placeholder="Secret in hex will be displayed here" /> | ||||||
|  |     </n-form-item> | ||||||
|  |  | ||||||
|  |     <n-form-item label="Epoch"> | ||||||
|  |       <input-copyable | ||||||
|  |         :value="Math.floor(now / 1000).toString()" | ||||||
|  |         readonly | ||||||
|  |         placeholder="Epoch in sec will be displayed here" | ||||||
|  |       /> | ||||||
|  |     </n-form-item> | ||||||
|  |     <n-form-item label="Iteration" :show-feedback="false"> | ||||||
|  |       <n-input-group> | ||||||
|  |         <n-input-group-label style="width: 110px">Count:</n-input-group-label> | ||||||
|  |         <input-copyable | ||||||
|  |           :value="String(getCounterFromTime({ now, timeStep: 30 }))" | ||||||
|  |           readonly | ||||||
|  |           placeholder="Iteration count will be displayed here" | ||||||
|  |         /> | ||||||
|  |       </n-input-group> | ||||||
|  |     </n-form-item> | ||||||
|  |  | ||||||
|  |     <n-form-item label="Iteration" :show-label="false" style="margin-top: 5px"> | ||||||
|  |       <n-input-group> | ||||||
|  |         <n-input-group-label style="width: 110px">Padded hex:</n-input-group-label> | ||||||
|  |         <input-copyable | ||||||
|  |           :value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')" | ||||||
|  |           readonly | ||||||
|  |           placeholder="Iteration count in hex will be displayed here" | ||||||
|  |         /> | ||||||
|  |       </n-input-group> | ||||||
|  |     </n-form-item> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref, watch } from 'vue'; | ||||||
|  | import { Refresh } from '@vicons/tabler'; | ||||||
|  | import { useTimestamp, whenever } from '@vueuse/core'; | ||||||
|  | import { useThemeVars } from 'naive-ui'; | ||||||
|  | import { useStyleStore } from '@/stores/style.store'; | ||||||
|  | import InputCopyable from '@/components/InputCopyable.vue'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
|  | import { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service'; | ||||||
|  | import { useQRCode } from '../qr-code-generator/useQRCode'; | ||||||
|  | import TokenDisplay from './token-display.vue'; | ||||||
|  |  | ||||||
|  | const now = useTimestamp(); | ||||||
|  | const interval = computed(() => (now.value / 1000) % 30); | ||||||
|  | const theme = useThemeVars(); | ||||||
|  | const styleStore = useStyleStore(); | ||||||
|  | const secret = ref(generateSecret()); | ||||||
|  | const tokens = ref(buildTokens()); | ||||||
|  | const keyUri = computed(() => buildKeyUri({ secret: secret.value })); | ||||||
|  |  | ||||||
|  | const { qrcode } = useQRCode({ | ||||||
|  |   text: keyUri, | ||||||
|  |   color: { background: '#00000000', foreground: computed(() => (styleStore.isDarkTheme ? '#ffffff' : '#000000')) }, | ||||||
|  |   options: { width: 210 }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const { attrs: secretValidationAttrs } = useValidation({ | ||||||
|  |   source: secret, | ||||||
|  |   rules: [ | ||||||
|  |     { | ||||||
|  |       message: 'Secret should be a base32 string', | ||||||
|  |       validator: (value) => value.match(/^[A-Z234567]+$/), | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       message: 'Please set a secret', | ||||||
|  |       validator: (value) => value !== '', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // watch + whenever to prevent token to be refresh every raf | ||||||
|  | watch([secret], refreshToken); | ||||||
|  | whenever(() => Math.floor(interval.value) === 0, refreshToken); | ||||||
|  |  | ||||||
|  | function refreshSecret() { | ||||||
|  |   secret.value = generateSecret(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function refreshToken() { | ||||||
|  |   tokens.value = buildTokens(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function buildTokens() { | ||||||
|  |   return { | ||||||
|  |     previous: generateTOTP({ key: secret.value, now: now.value - 30000 }), | ||||||
|  |     current: generateTOTP({ key: secret.value, now: now.value }), | ||||||
|  |     next: generateTOTP({ key: secret.value, now: now.value + 30000 }), | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="less" scoped> | ||||||
|  | .n-progress { | ||||||
|  |   margin-top: 10px; | ||||||
|  |   ::v-deep(.n-progress-graph-line-fill) { | ||||||
|  |     transition-duration: 0.05s !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .token { | ||||||
|  |   text-align: center; | ||||||
|  |   &.token-current { | ||||||
|  |     font-size: 20px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										124
									
								
								src/tools/otp-code-generator-and-validator/otp.service.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/tools/otp-code-generator-and-validator/otp.service.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | |||||||
|  | import { describe, expect, it } from 'vitest'; | ||||||
|  | import { | ||||||
|  |   generateHOTP, | ||||||
|  |   hexToBytes, | ||||||
|  |   verifyHOTP, | ||||||
|  |   generateTOTP, | ||||||
|  |   verifyTOTP, | ||||||
|  |   buildKeyUri, | ||||||
|  |   base32toHex, | ||||||
|  | } from './otp.service'; | ||||||
|  |  | ||||||
|  | describe('otp functions', () => { | ||||||
|  |   describe('hexToBytes', () => { | ||||||
|  |     it('convert an hexstring to a byte array', () => { | ||||||
|  |       expect(hexToBytes('1')).to.eql([1]); | ||||||
|  |       expect(hexToBytes('ffffff')).to.eql([255, 255, 255]); | ||||||
|  |       expect(hexToBytes('000000000')).to.eql([0, 0, 0, 0, 0]); | ||||||
|  |       expect(hexToBytes('a3218bcef89')).to.eql([163, 33, 139, 206, 248, 9]); | ||||||
|  |       expect(hexToBytes('063679ca')).toEqual([6, 54, 121, 202]); | ||||||
|  |       expect(hexToBytes('0102030405060708090a0b0c0d0e0f')).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   describe('base32tohex', () => { | ||||||
|  |     it('convert a base32 to hex string', () => { | ||||||
|  |       expect(base32toHex('ABCDEF')).to.eql('00443205'); | ||||||
|  |       expect(base32toHex('7777')).to.eql('ffff0f'); | ||||||
|  |       expect(base32toHex('JBSWY3DPEHPK3PXP')).to.eql('48656c6c6f21deadbeef'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('generateHOTP', () => { | ||||||
|  |     it('generates HOTP codes for a given counter', () => { | ||||||
|  |       const key = 'JBSWY3DPEHPK3PXP'; | ||||||
|  |       const hotpCodes = ['282760', '996554', '602287', '143627', '960129']; | ||||||
|  |  | ||||||
|  |       for (const [counter, code] of hotpCodes.entries()) { | ||||||
|  |         expect(generateHOTP({ key, counter })).to.eql(code); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('verifyHOTP', () => { | ||||||
|  |     it('validate hotp for a given secret', () => { | ||||||
|  |       const key = 'JBSWY3DPEHPK3PXP'; | ||||||
|  |       const hotpCodes = ['282760', '996554', '602287', '143627', '960129']; | ||||||
|  |  | ||||||
|  |       for (const [counter, token] of hotpCodes.entries()) { | ||||||
|  |         expect(verifyHOTP({ token, key, counter, window: 0 })).to.eql(true); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       expect(verifyHOTP({ token: 'INVALID', key })).to.eql(false); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('does not validate hotp out of sync', () => { | ||||||
|  |       const key = 'JBSWY3DPEHPK3PXP'; | ||||||
|  |       const token = '282760'; | ||||||
|  |  | ||||||
|  |       expect(verifyHOTP({ token, key, counter: 5, window: 2 })).to.eql(false); | ||||||
|  |       expect(verifyHOTP({ token, key, counter: 5, window: 5 })).to.eql(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('generateTOTP', () => { | ||||||
|  |     it('generates TOTP codes', () => { | ||||||
|  |       const key = 'JBSWY3DPEHPK3PXP'; | ||||||
|  |  | ||||||
|  |       const codes = [ | ||||||
|  |         { token: '282760', now: 0 }, | ||||||
|  |         { token: '341128', now: 1465324707000 }, | ||||||
|  |         { token: '089029', now: 1365324707000 }, | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       for (const { token, now } of codes) { | ||||||
|  |         expect(generateTOTP({ key, now })).to.eql(token); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('verifyTOTP', () => { | ||||||
|  |     it('verify TOTP in sync codes against a key', () => { | ||||||
|  |       const key = 'JBSWY3DPEHPK3PXP'; | ||||||
|  |  | ||||||
|  |       const codes = [ | ||||||
|  |         { token: '282760', now: 0 }, | ||||||
|  |         { token: '341128', now: 1465324707000 }, | ||||||
|  |         { token: '089029', now: 1365324707000 }, | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       for (const { token, now } of codes) { | ||||||
|  |         expect(verifyTOTP({ key, token, now })).to.eql(true); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('does not validate totp out of sync', () => { | ||||||
|  |       const key = 'JBSWY3DPEHPK3PXP'; | ||||||
|  |       const token = '635183'; | ||||||
|  |       const now = 1661266455000; | ||||||
|  |  | ||||||
|  |       expect(verifyTOTP({ key, token, now, window: 2 })).to.eql(true); | ||||||
|  |       expect(verifyTOTP({ key, token, now, window: 1 })).to.eql(false); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('buildKeyUri', () => { | ||||||
|  |     it('build a key uri string', () => { | ||||||
|  |       expect(buildKeyUri({ secret: 'JBSWY3DPEHPK3PXP' })).to.eql( | ||||||
|  |         'otpauth://totp/IT-Tools:demo-user?issuer=IT-Tools&secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       expect( | ||||||
|  |         buildKeyUri({ | ||||||
|  |           secret: 'JBSWY3DPEHPK3PXP', | ||||||
|  |           app: 'app-name', | ||||||
|  |           account: 'account', | ||||||
|  |           algorithm: 'algo', | ||||||
|  |           digits: 7, | ||||||
|  |           period: 10, | ||||||
|  |         }), | ||||||
|  |       ).to.eql( | ||||||
|  |         'otpauth://totp/app-name:account?issuer=app-name&secret=JBSWY3DPEHPK3PXP&algorithm=algo&digits=7&period=10', | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										139
									
								
								src/tools/otp-code-generator-and-validator/otp.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/tools/otp-code-generator-and-validator/otp.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | |||||||
|  | import { enc, HmacSHA1 } from 'crypto-js'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import { createToken } from '../token-generator/token-generator.service'; | ||||||
|  |  | ||||||
|  | export { | ||||||
|  |   generateHOTP, | ||||||
|  |   hexToBytes, | ||||||
|  |   verifyHOTP, | ||||||
|  |   generateTOTP, | ||||||
|  |   verifyTOTP, | ||||||
|  |   buildKeyUri, | ||||||
|  |   generateSecret, | ||||||
|  |   base32toHex, | ||||||
|  |   getCounterFromTime, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function hexToBytes(hex: string) { | ||||||
|  |   return (hex.match(/.{1,2}/g) ?? []).map((char) => parseInt(char, 16)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function computeHMACSha1(message: string, key: string) { | ||||||
|  |   return HmacSHA1(enc.Hex.parse(message), enc.Hex.parse(base32toHex(key))).toString(enc.Hex); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function base32toHex(base32: string) { | ||||||
|  |   const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; | ||||||
|  |  | ||||||
|  |   const bits = base32 | ||||||
|  |     .replace(/=+$/, '') | ||||||
|  |     .split('') | ||||||
|  |     .map((value) => base32Chars.indexOf(value).toString(2).padStart(5, '0')) | ||||||
|  |     .join(''); | ||||||
|  |  | ||||||
|  |   const hex = (bits.match(/.{1,8}/g) ?? []).map((chunk) => parseInt(chunk, 2).toString(16).padStart(2, '0')).join(''); | ||||||
|  |  | ||||||
|  |   return hex; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function generateHOTP({ key, counter = 0 }: { key: string; counter?: number }) { | ||||||
|  |   // Compute HMACdigest | ||||||
|  |   const digest = computeHMACSha1(counter.toString(16).padStart(16, '0'), key); | ||||||
|  |  | ||||||
|  |   // Get byte array | ||||||
|  |   const bytes = hexToBytes(digest); | ||||||
|  |  | ||||||
|  |   // Truncate | ||||||
|  |   const offset = bytes[19] & 0xf; | ||||||
|  |   const v = | ||||||
|  |     ((bytes[offset] & 0x7f) << 24) | | ||||||
|  |     ((bytes[offset + 1] & 0xff) << 16) | | ||||||
|  |     ((bytes[offset + 2] & 0xff) << 8) | | ||||||
|  |     (bytes[offset + 3] & 0xff); | ||||||
|  |  | ||||||
|  |   const code = String(v % 1000000).padStart(6, '0'); | ||||||
|  |  | ||||||
|  |   return code; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function verifyHOTP({ | ||||||
|  |   token, | ||||||
|  |   key, | ||||||
|  |   window = 0, | ||||||
|  |   counter = 0, | ||||||
|  | }: { | ||||||
|  |   token: string; | ||||||
|  |   key: string; | ||||||
|  |   window?: number; | ||||||
|  |   counter?: number; | ||||||
|  | }) { | ||||||
|  |   for (let i = counter - window; i <= counter + window; ++i) { | ||||||
|  |     if (generateHOTP({ key, counter: i }) === token) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getCounterFromTime({ now, timeStep }: { now: number; timeStep: number }) { | ||||||
|  |   return Math.floor(now / 1000 / timeStep); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function generateTOTP({ key, now = Date.now(), timeStep = 30 }: { key: string; now?: number; timeStep?: number }) { | ||||||
|  |   const counter = getCounterFromTime({ now, timeStep }); | ||||||
|  |  | ||||||
|  |   return generateHOTP({ key, counter }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function verifyTOTP({ | ||||||
|  |   key, | ||||||
|  |   token, | ||||||
|  |   window = 0, | ||||||
|  |   now = Date.now(), | ||||||
|  |   timeStep = 30, | ||||||
|  | }: { | ||||||
|  |   token: string; | ||||||
|  |   key: string; | ||||||
|  |   window?: number; | ||||||
|  |   now?: number; | ||||||
|  |   timeStep?: number; | ||||||
|  | }) { | ||||||
|  |   const counter = getCounterFromTime({ now, timeStep }); | ||||||
|  |  | ||||||
|  |   return verifyHOTP({ token, key, window, counter }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function buildKeyUri({ | ||||||
|  |   secret, | ||||||
|  |   app = 'IT-Tools', | ||||||
|  |   account = 'demo-user', | ||||||
|  |   algorithm = 'SHA1', | ||||||
|  |   digits = 6, | ||||||
|  |   period = 30, | ||||||
|  | }: { | ||||||
|  |   secret: string; | ||||||
|  |   app?: string; | ||||||
|  |   account?: string; | ||||||
|  |   algorithm?: string; | ||||||
|  |   digits?: number; | ||||||
|  |   period?: number; | ||||||
|  | }) { | ||||||
|  |   const params = { | ||||||
|  |     issuer: app, | ||||||
|  |     secret, | ||||||
|  |     algorithm, | ||||||
|  |     digits, | ||||||
|  |     period, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const paramsString = _(params) | ||||||
|  |     .map((value, key) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) | ||||||
|  |     .join('&'); | ||||||
|  |  | ||||||
|  |   return `otpauth://totp/${encodeURIComponent(app)}:${encodeURIComponent(account)}?${paramsString}`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function generateSecret() { | ||||||
|  |   return createToken({ length: 16, alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' }); | ||||||
|  | } | ||||||
							
								
								
									
										66
									
								
								src/tools/otp-code-generator-and-validator/token-display.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/tools/otp-code-generator-and-validator/token-display.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <n-space class="labels" item-style="flex: 1 1 0" style="width: 100%" align="center"> | ||||||
|  |       <div style="text-align: left">Previous</div> | ||||||
|  |       <div style="text-align: center">Current OTP</div> | ||||||
|  |       <div style="text-align: right">Next</div> | ||||||
|  |     </n-space> | ||||||
|  |     <n-input-group> | ||||||
|  |       <n-tooltip trigger="hover" placement="bottom"> | ||||||
|  |         <template #trigger> | ||||||
|  |           <n-button secondary @click.prevent="copyPrevious(tokens.previous)">{{ tokens.previous }}</n-button> | ||||||
|  |         </template> | ||||||
|  |         <div>{{ previousCopied ? 'Copied !' : 'Copy previous OTP' }}</div> | ||||||
|  |       </n-tooltip> | ||||||
|  |       <n-tooltip trigger="hover" placement="bottom"> | ||||||
|  |         <template #trigger> | ||||||
|  |           <n-button tertiary type="primary" class="current-otp" @click.prevent="copyCurrent(tokens.current)"> | ||||||
|  |             {{ tokens.current }} | ||||||
|  |           </n-button> | ||||||
|  |         </template> | ||||||
|  |         <div>{{ currentCopied ? 'Copied !' : 'Copy current OTP' }}</div> | ||||||
|  |       </n-tooltip> | ||||||
|  |       <n-tooltip trigger="hover" placement="bottom"> | ||||||
|  |         <template #trigger> | ||||||
|  |           <n-button secondary @click.prevent="copyNext(tokens.next)">{{ tokens.next }}</n-button> | ||||||
|  |         </template> | ||||||
|  |         <div>{{ nextCopied ? 'Copied !' : 'Copy next OTP' }}</div> | ||||||
|  |       </n-tooltip> | ||||||
|  |     </n-input-group> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useClipboard } from '@vueuse/core'; | ||||||
|  | import { toRefs } from 'vue'; | ||||||
|  |  | ||||||
|  | const { copy: copyPrevious, copied: previousCopied } = useClipboard(); | ||||||
|  | const { copy: copyCurrent, copied: currentCopied } = useClipboard(); | ||||||
|  | const { copy: copyNext, copied: nextCopied } = useClipboard(); | ||||||
|  |  | ||||||
|  | const props = defineProps<{ tokens: { previous: string; current: string; next: string } }>(); | ||||||
|  | const { tokens } = toRefs(props); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped lang="less"> | ||||||
|  | .current-otp { | ||||||
|  |   font-size: 22px; | ||||||
|  |   flex: 1 0 35% !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .n-button { | ||||||
|  |   height: 45px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .labels { | ||||||
|  |   div { | ||||||
|  |     text-align: center; | ||||||
|  |     padding: 0 2px 6px 2px; | ||||||
|  |     line-height: 1.25; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .n-input-group > * { | ||||||
|  |   flex: 1 0 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -1,5 +1,6 @@ | |||||||
|  | import { get, type MaybeRef } from '@vueuse/core'; | ||||||
| import QRCode, { type QRCodeErrorCorrectionLevel, type QRCodeToDataURLOptions } from 'qrcode'; | import QRCode, { type QRCodeErrorCorrectionLevel, type QRCodeToDataURLOptions } from 'qrcode'; | ||||||
| import { ref, watch, type Ref } from 'vue'; | import { ref, watch, isRef } from 'vue'; | ||||||
|  |  | ||||||
| export function useQRCode({ | export function useQRCode({ | ||||||
|   text, |   text, | ||||||
| @@ -7,24 +8,24 @@ export function useQRCode({ | |||||||
|   errorCorrectionLevel, |   errorCorrectionLevel, | ||||||
|   options, |   options, | ||||||
| }: { | }: { | ||||||
|   text: Ref<string>; |   text: MaybeRef<string>; | ||||||
|   color: { foreground: Ref<string>; background: Ref<string> }; |   color: { foreground: MaybeRef<string>; background: MaybeRef<string> }; | ||||||
|   errorCorrectionLevel: Ref<QRCodeErrorCorrectionLevel>; |   errorCorrectionLevel?: MaybeRef<QRCodeErrorCorrectionLevel>; | ||||||
|   options?: QRCodeToDataURLOptions; |   options?: QRCodeToDataURLOptions; | ||||||
| }) { | }) { | ||||||
|   const qrcode = ref(''); |   const qrcode = ref(''); | ||||||
|  |  | ||||||
|   watch( |   watch( | ||||||
|     [text, background, foreground, errorCorrectionLevel], |     [text, background, foreground, errorCorrectionLevel].filter(isRef), | ||||||
|     async () => { |     async () => { | ||||||
|       if (text.value) |       if (get(text)) | ||||||
|         qrcode.value = await QRCode.toDataURL(text.value, { |         qrcode.value = await QRCode.toDataURL(get(text), { | ||||||
|           color: { |           color: { | ||||||
|             dark: foreground.value, |             dark: get(foreground), | ||||||
|             light: background.value, |             light: get(background), | ||||||
|             ...options?.color, |             ...options?.color, | ||||||
|           }, |           }, | ||||||
|           errorCorrectionLevel: errorCorrectionLevel.value, |           errorCorrectionLevel: get(errorCorrectionLevel) ?? 'M', | ||||||
|           ...options, |           ...options, | ||||||
|         }); |         }); | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -6,19 +6,23 @@ export function createToken({ | |||||||
|   withNumbers = true, |   withNumbers = true, | ||||||
|   withSymbols = false, |   withSymbols = false, | ||||||
|   length = 64, |   length = 64, | ||||||
|  |   alphabet, | ||||||
| }: { | }: { | ||||||
|   withUppercase?: boolean; |   withUppercase?: boolean; | ||||||
|   withLowercase?: boolean; |   withLowercase?: boolean; | ||||||
|   withNumbers?: boolean; |   withNumbers?: boolean; | ||||||
|   withSymbols?: boolean; |   withSymbols?: boolean; | ||||||
|   length?: number; |   length?: number; | ||||||
|  |   alphabet?: string; | ||||||
| }) { | }) { | ||||||
|   const alphabet = [ |   const allAlphabet = | ||||||
|  |     alphabet ?? | ||||||
|  |     [ | ||||||
|       ...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''), |       ...(withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : ''), | ||||||
|       ...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''), |       ...(withLowercase ? 'abcdefghijklmopqrstuvwxyz' : ''), | ||||||
|       ...(withNumbers ? '0123456789' : ''), |       ...(withNumbers ? '0123456789' : ''), | ||||||
|       ...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''), |       ...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : ''), | ||||||
|     ].join(''); |     ].join(''); | ||||||
|  |  | ||||||
|   return shuffleString(alphabet.repeat(length)).substring(0, length); |   return shuffleString(allAlphabet.repeat(length)).substring(0, length); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
|  | import _ from 'lodash'; | ||||||
| import { describe, expect, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { isNotThrowing } from './boolean'; | import { isNotThrowing } from './boolean'; | ||||||
|  |  | ||||||
| describe('boolean utils', () => { | describe('boolean utils', () => { | ||||||
|   describe('isNotThrowing', () => { |   describe('isNotThrowing', () => { | ||||||
|     it('should return if the call throws or false otherwise', () => { |     it('should return if the call throws or false otherwise', () => { | ||||||
|       expect(isNotThrowing(() => {})).to.eql(true); |       expect(isNotThrowing(_.noop)).to.eql(true); | ||||||
|       expect( |       expect( | ||||||
|         isNotThrowing(() => { |         isNotThrowing(() => { | ||||||
|           throw new Error(); |           throw new Error(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user