feat: QR for one time tokens

This commit is contained in:
Phan An 2024-04-03 16:48:52 +02:00
parent 1cb89b3a5f
commit 9d2a3afca9
14 changed files with 367 additions and 45 deletions

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\AuthenticationService;
use Illuminate\Contracts\Auth\Authenticatable;
class GetOneTimeTokenController extends Controller
{
/** @var User $user */
public function __invoke(AuthenticationService $auth, Authenticatable $user)
{
return response()->json(['token' => $auth->generateOneTimeToken($user)]);
}
}

View file

@ -9,6 +9,7 @@ use App\Values\CompositeToken;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Auth\Passwords\PasswordBroker;
use Illuminate\Hashing\HashManager;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Password;
class AuthenticationService
@ -70,4 +71,12 @@ class AuthenticationService
return $status === Password::PASSWORD_RESET;
}
public function generateOneTimeToken(User $user): string
{
$token = bin2hex(random_bytes(16));
Cache::set("one-time-token.$user->id", $token, 60 * 10);
return $token;
}
}

View file

@ -52,6 +52,7 @@
"@typescript-eslint/parser": "^4.11.1",
"@vitejs/plugin-vue": "^5.0.4",
"@vueuse/core": "^10.9.0",
"@vueuse/integrations": "^10.9.0",
"add": "^2.0.6",
"cross-env": "^7.0.3",
"cypress": "^9.5.4",
@ -71,6 +72,7 @@
"laravel-vite-plugin": "^1.0.2",
"lint-staged": "^10.3.0",
"lucide-vue-next": "^0.363.0",
"qrcode": "^1",
"sass": "^1.72.0",
"start-server-and-test": "^2.0.3",
"typescript": "^4.8.4",

View file

@ -0,0 +1,29 @@
<template>
<div class="integrations">
<article>
<SpotifyIntegration />
</article>
<article>
<LastfmIntegration />
</article>
</div>
</template>
<script setup lang="ts">
import LastfmIntegration from '@/components/profile-preferences/LastfmIntegration.vue'
import SpotifyIntegration from '@/components/profile-preferences/SpotifyIntegration.vue'
</script>
<style scoped lang="scss">
.integrations {
> article + article {
margin-top: 2rem;
}
:deep(h3) {
font-size: 1.8rem;
margin-bottom: 1rem;
}
}
</style>

View file

@ -1,11 +1,11 @@
<template>
<section class="text-secondary">
<h1>
<h3>
<span class="lastfm-icon">
<Icon :icon="faLastfm" />
</span>
Last.fm Integration
</h1>
</h3>
<div v-if="useLastfm" data-testid="lastfm-integrated">
<p>Last.fm integration is enabled. Koel will attempt to retrieve album and artist information from Last.fm.</p>

View file

@ -131,12 +131,6 @@ const update = async () => {
<style lang="scss" scoped>
form {
width: 66%;
@media (max-width: 1024px) {
width: 100%;
}
input {
width: 100%;
}

View file

@ -0,0 +1,46 @@
<template>
<article class="text-secondary">
Instead of using a password, you can scan the QR code below to log in to
<a href="https://koel.dev/#mobile" target="_blank" class="text-highlight">Koel Player</a>
on your mobile device.<br>
The QR code will refresh every 10 minutes.
<img :src="qrCodeUrl" alt="QR Code" width="192" height="192">
</article>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { useQRCode } from '@vueuse/integrations/useQRCode'
import { authService } from '@/services'
import { base64Encode } from '@/utils'
const qrCodeData = ref('')
const oneTimeToken = ref('')
watch(oneTimeToken, () => {
qrCodeData.value = base64Encode(JSON.stringify({
token: oneTimeToken.value,
url: window.BASE_URL
}))
})
const qrCodeUrl = useQRCode(qrCodeData, {
width: window.devicePixelRatio === 1 ? 196 : 384,
height: window.devicePixelRatio === 1 ? 196 : 384
})
const resetOneTimeToken = async () => (oneTimeToken.value = await authService.getOneTimeToken())
onMounted(() => {
window.setInterval(resetOneTimeToken, 60 * 10 * 1000)
resetOneTimeToken()
})
</script>
<style scoped lang="scss">
img {
margin-top: 1.5rem;
display: block;
border-radius: 8px;
}
</style>

View file

@ -1,11 +1,11 @@
<template>
<section class="text-secondary">
<h1>
<h3>
<span class="spotify-icon">
<Icon :icon="faSpotify" />
</span>
Spotify Integration
</h1>
</h3>
<div v-if="useSpotify">
<p>

View file

@ -1,6 +1,5 @@
<template>
<section>
<h1>Theme</h1>
<ul class="themes">
<li v-for="theme in themes" :key="theme.id" data-testid="theme-card">
<ThemeCard :key="theme.id" :theme="theme" @selected="setTheme" />
@ -24,11 +23,15 @@ const setTheme = (theme: Theme) => themeStore.setTheme(theme)
.themes {
display: grid;
grid-auto-rows: 8rem;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
grid-template-columns: auto auto auto;
grid-gap: .75rem 1rem;
@media only screen and (max-width: 667px) {
grid-template-columns: 1fr;
@media only screen and (max-width: 768px) {
grid-template-columns: auto auto;
}
@media only screen and (max-width: 480px) {
grid-template-columns: auto;
}
}
</style>

View file

@ -3,35 +3,121 @@
<ScreenHeader>Profile &amp; Preferences</ScreenHeader>
<div class="main-scroll-wrap">
<ProfileForm />
<ThemeList />
<PreferencesForm />
<SpotifyIntegration />
<LastfmIntegration />
<main class="tabs">
<div class="clear" role="tablist">
<button
role="tab"
:aria-selected="currentTab === 'profile'"
aria-controls="profilePaneProfile"
@click.prevent="currentTab = 'profile'"
>
Profile
</button>
<button
role="tab"
:aria-selected="currentTab === 'preferences'"
aria-controls="profilePanePreferences"
@click.prevent="currentTab = 'preferences'"
>
Preferences
</button>
<button
role="tab"
:aria-selected="currentTab === 'themes'"
aria-controls="profilePaneThemes"
@click.prevent="currentTab = 'themes'"
>
Themes
</button>
<button
role="tab"
:aria-selected="currentTab === 'integrations'"
aria-controls="profilePaneIntegrations"
@click.prevent="currentTab = 'integrations'"
>
Integrations
</button>
<button
role="tab"
:aria-selected="currentTab === 'qr'"
aria-controls="profilePaneQr"
@click.prevent="currentTab = 'qr'"
>
<QrCodeIcon :size="16" />
</button>
</div>
<div class="panes">
<div
v-show="currentTab === 'profile'"
id="profilePaneProfile"
role="tabpanel"
aria-labelledby="profilePaneProfile"
tabindex="0"
>
<ProfileForm />
</div>
<div
v-if="currentTab === 'preferences'"
id="profilePanePreferences"
role="tabpanel"
aria-labelledby="profilePanePreferences"
tabindex="0"
>
<PreferencesForm />
</div>
<div
v-if="currentTab === 'themes'"
id="profilePaneThemes"
role="tabpanel"
tabindex="0"
aria-labelledby="profilePaneThemes"
>
<ThemeList />
</div>
<div
v-if="currentTab === 'integrations'"
id="profilePaneIntegrations"
role="tabpanel"
tabindex="0"
aria-labelledby="profilePaneIntegrations"
>
<Integrations />
</div>
<div
v-if="currentTab === 'qr'"
id="profilePaneQr"
role="tabpanel"
aria-labelledby="profilePaneQr"
tabindex="0"
>
<QRLogin />
</div>
</div>
</main>
</div>
</section>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { QrCodeIcon } from 'lucide-vue-next'
import { useLocalStorage } from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import ProfileForm from '@/components/profile-preferences/ProfileForm.vue'
import SpotifyIntegration from '@/components/profile-preferences/SpotifyIntegration.vue'
import LastfmIntegration from '@/components/profile-preferences/LastfmIntegration.vue'
import PreferencesForm from '@/components/profile-preferences/PreferencesForm.vue'
import ThemeList from '@/components/profile-preferences/ThemeList.vue'
import Integrations from '@/components/profile-preferences/Integrations.vue'
import QRLogin from '@/components/profile-preferences/QRLogin.vue'
const { get, set } = useLocalStorage()
const currentTab = ref(get<'profile' | 'preferences' | 'themes' | 'integrations' | 'qr'>('profileScreenTab', 'profile'))
watch(currentTab, tab => set('profileScreenTab', tab))
</script>
<style lang="scss">
#profileWrapper {
.main-scroll-wrap > * + * {
margin-top: 1.75rem;
padding-top: 1.75rem;
border-top: 1px solid var(--color-background-secondary);
}
.main-scroll-wrap h1 {
font-size: 1.85rem;
margin-bottom: 1.25rem;
}
}
</style>

View file

@ -61,5 +61,7 @@ export const authService = {
resetPassword: async (email: string, password: string, token: string) => {
await http.post('reset-password', { email, password, token })
}
},
getOneTimeToken: async () => (await http.get<{ token: string }>('one-time-token')).token
}

View file

@ -180,13 +180,15 @@ label {
[role=tablist] {
border-bottom: 2px solid rgba(255, 255, 255, .1);
padding: 0 1.25rem;
display: flex;
gap: 0.3rem;
[role=tab] {
position: relative;
padding: .7rem 1.3rem;
margin-right: 0.2rem;
border-radius: 0;
opacity: .5;
border-radius: 4px 4px 0 0;
opacity: .7;
background: rgba(255, 255, 255, .05);
text-transform: uppercase;
color: var(--color-text-secondary);
@ -196,11 +198,8 @@ label {
background: rgba(255, 255, 255, .1);
}
&:first-child {
margin-left: 1.25rem;
}
&[aria-selected=true] {
transition: none;
color: var(--color-text-primary);
background: rgba(255, 255, 255, .1);
opacity: 1;

View file

@ -23,6 +23,7 @@ use App\Http\Controllers\API\FetchSongsForQueueController;
use App\Http\Controllers\API\ForgotPasswordController;
use App\Http\Controllers\API\GenreController;
use App\Http\Controllers\API\GenreSongController;
use App\Http\Controllers\API\GetOneTimeTokenController;
use App\Http\Controllers\API\LambdaSongController as S3SongController;
use App\Http\Controllers\API\LikeMultipleSongsController;
use App\Http\Controllers\API\MovePlaylistSongsController;
@ -73,6 +74,7 @@ Route::prefix('api')->middleware('api')->group(static function (): void {
Route::post('invitations/accept', [UserInvitationController::class, 'accept']);
Route::middleware('auth')->group(static function (): void {
Route::get('one-time-token', GetOneTimeTokenController::class);
Route::post('broadcasting/auth', static function (Request $request) {
$pusher = new Pusher(
config('broadcasting.connections.pusher.key'),

135
yarn.lock
View file

@ -1069,7 +1069,7 @@
"@vueuse/shared" "10.9.0"
vue-demi ">=0.14.7"
"@vueuse/integrations@^10.7.2":
"@vueuse/integrations@^10.7.2", "@vueuse/integrations@^10.9.0":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-10.9.0.tgz#2b1a9556215ad3c1f96d39cbfbef102cf6e0ec05"
integrity sha512-acK+A01AYdWSvL4BZmCoJAcyHJ6EqhmkQEXbQLwev1MY7NBnS+hcEMx/BzVoR9zKI+UqEPMD9u6PsyAuiTRT4Q==
@ -1442,6 +1442,11 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@ -1546,6 +1551,15 @@ cli-truncate@^2.1.0:
slice-ansi "^3.0.0"
string-width "^4.2.0"
cliui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -1788,6 +1802,11 @@ debug@^3.1.0, debug@^3.2.7:
dependencies:
ms "^2.1.1"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
decimal.js@^10.3.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.1.tgz#be75eeac4a2281aace80c1a8753587c27ef053e7"
@ -1844,6 +1863,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dijkstrajs@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23"
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@ -1910,6 +1934,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
encode-utf8@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@ -2554,6 +2583,14 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
dependencies:
locate-path "^5.0.0"
path-exists "^4.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
@ -2679,6 +2716,11 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-func-name@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
@ -3480,6 +3522,13 @@ local-storage@^2.0.0:
resolved "https://registry.yarnpkg.com/local-storage/-/local-storage-2.0.0.tgz#748b7d041b197f46f3ec7393640851c175b64db8"
integrity sha512-/0sRoeijw7yr/igbVVygDuq6dlYCmtsuTmmpnweVlVtl/s10pf5BCq8LWBxW/AMyFJ3MhMUuggMZiYlx6qr9tw==
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
dependencies:
p-locate "^4.1.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
@ -3801,6 +3850,13 @@ ospath@^1.2.2:
resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
dependencies:
p-try "^2.0.0"
p-limit@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
@ -3808,6 +3864,13 @@ p-limit@^3.0.2:
dependencies:
yocto-queue "^0.1.0"
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
p-limit "^2.2.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
@ -3822,6 +3885,11 @@ p-map@^4.0.0:
dependencies:
aggregate-error "^3.0.0"
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@ -3930,6 +3998,11 @@ plyr@1.5.x:
resolved "https://registry.yarnpkg.com/plyr/-/plyr-1.5.20.tgz#e97d620cb6c37735d3c0fffef7afa9f2ed82a854"
integrity sha512-YwI0jNfRrWz+t3b28rrOhm1VpzmNl8bCsFGQ/wJaU/8utyrFNX9AEZi2GAvSO5ctODLKt424RQs2CQ1SY+YZfg==
pngjs@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
postcss-selector-parser@^6.0.9:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
@ -4049,6 +4122,16 @@ pusher-js@^4.1.0:
tweetnacl-util "^0.15.0"
xmlhttprequest "^1.8.0"
qrcode@^1:
version "1.5.3"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170"
integrity sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==
dependencies:
dijkstrajs "^1.0.1"
encode-utf8 "^1.0.3"
pngjs "^5.0.0"
yargs "^15.3.1"
qs@~6.5.2:
version "6.5.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
@ -4102,6 +4185,16 @@ request-progress@^3.0.0:
dependencies:
throttleit "^1.0.0"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@ -4268,6 +4361,11 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.7:
dependencies:
lru-cache "^6.0.0"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -4950,6 +5048,11 @@ which-collection@^1.0.1:
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
which-pm-runs@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35"
@ -5022,6 +5125,11 @@ xmlhttprequest@^1.8.0:
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
integrity sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
@ -5037,6 +5145,31 @@ yaml@^1.10.0:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs@^15.3.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.2"
yarn@^1.22.22:
version "1.22.22"
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.22.tgz#ac34549e6aa8e7ead463a7407e1c7390f61a6610"