Compare commits
1 Commits
main
...
cryptograp
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d582461e2 |
177
app/components/button/google-code.vue
Normal file
177
app/components/button/google-code.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
useTokenClient,
|
||||
type TokenClientSuccessResponse,
|
||||
type TokenClientErrorResponse
|
||||
} from "vue3-google-signin";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const listStore = useListStore();
|
||||
const crypto = useCryptoStore()
|
||||
|
||||
/**
|
||||
* Logique Google Drive (appDataFolder)
|
||||
*/
|
||||
const handleDriveKey = async (accessToken: string) => {
|
||||
const fileName = 'list_master_key';
|
||||
const baseUrl = 'https://www.googleapis.com/drive/v3/files';
|
||||
const authHeader = { Authorization: `Bearer ${accessToken}` };
|
||||
|
||||
try {
|
||||
const searchParams = new URLSearchParams({
|
||||
q: `name = '${fileName}'`,
|
||||
spaces: 'appDataFolder',
|
||||
fields: 'files(id, name)'
|
||||
});
|
||||
|
||||
const searchRes = await $fetch<any>(`${baseUrl}?${searchParams}`, { headers: authHeader });
|
||||
|
||||
if (searchRes.files && searchRes.files.length > 0) {
|
||||
const fileId = searchRes.files[0].id;
|
||||
|
||||
// 1. Récupérer le contenu en tant que Blob ou ArrayBuffer pour préserver les bytes
|
||||
const blob = await $fetch<Blob>(`${baseUrl}/${fileId}?alt=media`, {
|
||||
headers: authHeader,
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
// Convertir le Blob en Uint8Array
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const keyUint8 = new Uint8Array(arrayBuffer);
|
||||
|
||||
listStore.setMasterKey(keyUint8);
|
||||
console.log("Clé binaire récupérée du Drive : " + keyUint8);
|
||||
} else {
|
||||
// 2. Création avec le type Uint8Array
|
||||
const newKey = crypto.generateMasterKey();
|
||||
|
||||
const metadata = {
|
||||
name: fileName,
|
||||
parents: ['appDataFolder']
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
// On spécifie explicitement 'application/octet-stream' pour la clé binaire
|
||||
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
||||
formData.append('file', new Blob([newKey as any], { type: 'application/octet-stream' }));
|
||||
|
||||
await $fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', {
|
||||
method: 'POST',
|
||||
headers: authHeader,
|
||||
body: formData
|
||||
});
|
||||
|
||||
listStore.setMasterKey(newKey);
|
||||
console.log("Nouvelle clé binaire générée et stockée : " + newKey);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Erreur Google Drive:", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Succès de l'authentification Google
|
||||
*/
|
||||
const handleSuccess = async (response: TokenClientSuccessResponse) => {
|
||||
const accessToken = response.access_token;
|
||||
console.log(response.access_token)
|
||||
|
||||
try {
|
||||
// // A. Vérification côté WordPress (Login)
|
||||
// // Note : On envoie l'accessToken ou on utilise l'ID Token si configuré.
|
||||
// // Si ton API WP attendait un 'code', il faudra l'adapter pour vérifier l'access_token ou l'email.
|
||||
const data = await $fetch<any>('/api-wp/auth/google', {
|
||||
method: 'POST',
|
||||
body: { access_token: accessToken }
|
||||
});
|
||||
|
||||
// Stockage des infos WP
|
||||
authStore.setTokenCookie(data.token);
|
||||
authStore.setUserCookie(data.user);
|
||||
|
||||
// B. Gestion de la clé sur Google Drive (Entièrement géré par le Front)
|
||||
await handleDriveKey(accessToken);
|
||||
listStore.processRawData(data.lists);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erreur login WordPress:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (error: TokenClientErrorResponse) => {
|
||||
console.error("Erreur Google:", error);
|
||||
};
|
||||
|
||||
// Initialisation du Token Client (Implicit Flow - Pas de secret nécessaire)
|
||||
const { login } = useTokenClient({
|
||||
scope: 'openid email profile https://www.googleapis.com/auth/drive.appdata',
|
||||
onSuccess: handleSuccess,
|
||||
onError: handleError,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="() => login()" class="g-signin-button">
|
||||
<div class="g-logo-wrapper">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" class="g-logo">
|
||||
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
|
||||
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
|
||||
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
|
||||
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
|
||||
<path fill="none" d="M0 0h48v48H0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="g-button-text">{{ $t('googleBtnTxt') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.g-signin-button {
|
||||
margin:auto;
|
||||
margin-top:1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
background-color: #ffffff;
|
||||
color: #757575;
|
||||
height: 40px;
|
||||
width: auto;
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 4px;
|
||||
padding: 0em;
|
||||
font-family: 'Roboto', arial, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 2px 0 rgba(60,64,67,0.302), 0 1px 3px 1px rgba(60,64,67,0.149);
|
||||
transition: background-color .218s, border-color .218s, box-shadow .218s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.g-signin-button:hover {
|
||||
background-color: hsl(210, 17%, 90%);
|
||||
border-color: #d2d2d2;
|
||||
box-shadow: 0 1px 2px 0 rgba(60,64,67,0.302), 0 1px 3px 1px rgba(60,64,67,0.149);
|
||||
}
|
||||
|
||||
.g-signin-button:active {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.g-logo-wrapper {
|
||||
padding: 11px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.g-logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.g-button-text {
|
||||
padding: 0 12px 0 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,7 @@
|
||||
authStore.setTokenCookie(data.token);
|
||||
authStore.setUserCookie(data.user);
|
||||
listStore.lists = data.lists
|
||||
console.log(listStore.lists) //ok
|
||||
//console.log(listStore.lists) //ok
|
||||
} catch (error) {
|
||||
console.error("Erreur côté WordPress:", error);
|
||||
}
|
||||
|
||||
199
app/composables/crypto.ts
Normal file
199
app/composables/crypto.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import sodium from "libsodium-wrappers"
|
||||
|
||||
export type KeyPair = {
|
||||
publicKey: Uint8Array
|
||||
privateKey: Uint8Array
|
||||
}
|
||||
|
||||
export type EncryptedPayload = {
|
||||
nonce: Uint8Array
|
||||
cipher: Uint8Array
|
||||
}
|
||||
|
||||
export type EncryptedPrivateKey = {
|
||||
nonce: Uint8Array
|
||||
cipher: Uint8Array
|
||||
salt: Uint8Array
|
||||
}
|
||||
|
||||
export const useCryptoStore = () => {
|
||||
|
||||
let ready = false
|
||||
|
||||
const init = async (): Promise<void> => {
|
||||
if (!ready) {
|
||||
await sodium.ready
|
||||
ready = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* USER KEY PAIR
|
||||
*/
|
||||
const generateUserKeyPair = (): KeyPair => {
|
||||
|
||||
const pair = sodium.crypto_box_keypair()
|
||||
|
||||
return {
|
||||
publicKey: pair.publicKey,
|
||||
privateKey: pair.privateKey
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MASTER KEY
|
||||
*/
|
||||
const generateMasterKey = (): Uint8Array => {
|
||||
return sodium.randombytes_buf(
|
||||
sodium.crypto_secretbox_KEYBYTES
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* ENCRYPT PRIVATE KEY
|
||||
*/
|
||||
const encryptPrivateKey = (
|
||||
privateKey: Uint8Array,
|
||||
masterKey: Uint8Array
|
||||
): EncryptedPayload => {
|
||||
|
||||
const nonce = sodium.randombytes_buf(
|
||||
sodium.crypto_secretbox_NONCEBYTES
|
||||
)
|
||||
|
||||
const cipher = sodium.crypto_secretbox_easy(
|
||||
privateKey,
|
||||
nonce,
|
||||
masterKey
|
||||
)
|
||||
|
||||
return { nonce, cipher }
|
||||
}
|
||||
|
||||
/**
|
||||
* DECRYPT PRIVATE KEY
|
||||
*/
|
||||
const decryptPrivateKey = (
|
||||
encrypted: EncryptedPayload,
|
||||
masterKey: Uint8Array
|
||||
): Uint8Array => {
|
||||
|
||||
return sodium.crypto_secretbox_open_easy(
|
||||
encrypted.cipher,
|
||||
encrypted.nonce,
|
||||
masterKey
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* LIST KEY
|
||||
*/
|
||||
const generateListKey = (): Uint8Array => {
|
||||
|
||||
return sodium.randombytes_buf(
|
||||
sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* ENCRYPT LIST DATA
|
||||
*/
|
||||
const encryptList = (
|
||||
data: string,
|
||||
listKey: Uint8Array
|
||||
): EncryptedPayload => {
|
||||
|
||||
const nonce = sodium.randombytes_buf(
|
||||
sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
|
||||
)
|
||||
|
||||
const cipher = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||
sodium.from_string(data),
|
||||
null,
|
||||
null,
|
||||
nonce,
|
||||
listKey
|
||||
)
|
||||
|
||||
return { nonce, cipher }
|
||||
}
|
||||
|
||||
/**
|
||||
* DECRYPT LIST DATA
|
||||
*/
|
||||
const decryptList = (
|
||||
encrypted: EncryptedPayload,
|
||||
listKey: Uint8Array
|
||||
): string => {
|
||||
|
||||
const decrypted = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
null,
|
||||
encrypted.cipher,
|
||||
null,
|
||||
encrypted.nonce,
|
||||
listKey
|
||||
)
|
||||
|
||||
return sodium.to_string(decrypted)
|
||||
}
|
||||
|
||||
/**
|
||||
* ENCRYPT LIST KEY FOR USER
|
||||
*/
|
||||
const encryptListKeyForUser = (
|
||||
listKey: Uint8Array,
|
||||
recipientPublicKey: Uint8Array,
|
||||
senderPrivateKey: Uint8Array
|
||||
): EncryptedPayload => {
|
||||
|
||||
const nonce = sodium.randombytes_buf(
|
||||
sodium.crypto_box_NONCEBYTES
|
||||
)
|
||||
|
||||
const cipher = sodium.crypto_box_easy(
|
||||
listKey,
|
||||
nonce,
|
||||
recipientPublicKey,
|
||||
senderPrivateKey
|
||||
)
|
||||
|
||||
return { nonce, cipher }
|
||||
}
|
||||
|
||||
/**
|
||||
* DECRYPT LIST KEY
|
||||
*/
|
||||
const decryptListKey = (
|
||||
encrypted: EncryptedPayload,
|
||||
senderPublicKey: Uint8Array,
|
||||
recipientPrivateKey: Uint8Array
|
||||
): Uint8Array => {
|
||||
|
||||
return sodium.crypto_box_open_easy(
|
||||
encrypted.cipher,
|
||||
encrypted.nonce,
|
||||
senderPublicKey,
|
||||
recipientPrivateKey
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
init,
|
||||
|
||||
generateUserKeyPair,
|
||||
|
||||
generateMasterKey,
|
||||
|
||||
encryptPrivateKey,
|
||||
decryptPrivateKey,
|
||||
|
||||
generateListKey,
|
||||
|
||||
encryptList,
|
||||
decryptList,
|
||||
|
||||
encryptListKeyForUser,
|
||||
decryptListKey
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
export const useCounter = () => {
|
||||
const counter = useState('counter', () => 0)
|
||||
const increment = () => {
|
||||
counter.value++
|
||||
}
|
||||
|
||||
const decrement = () => {
|
||||
counter.value--
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
counter.value=0
|
||||
}
|
||||
return {
|
||||
counter,
|
||||
increment,
|
||||
decrement,
|
||||
reset
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ const handleFormSubmit = async() => {
|
||||
</ButtonBase>
|
||||
</form>
|
||||
<p>{{ $t('loginGoogle') }}</p>
|
||||
<ButtonGoogle/>
|
||||
<ButtonGoogleCode/>
|
||||
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { $Fetch } from 'ofetch';
|
||||
import type { List } from '~/types/lists'
|
||||
import type { List, WPListEncrypted } from '~/types/lists'
|
||||
|
||||
export default class ListRepository {
|
||||
private fetcher: $Fetch;
|
||||
@@ -17,6 +17,23 @@ export default class ListRepository {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
}
|
||||
|
||||
async uploadKeys(data: string) {
|
||||
console.log('uploadEncryptedPrivateKey : data is')
|
||||
console.log(data)
|
||||
|
||||
return await this.fetcher<any>('/lists/PrivKeyCipher', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
}
|
||||
|
||||
async update(data: WPListEncrypted) {
|
||||
const string = JSON.stringify(data);
|
||||
return await this.fetcher<any>('/lists/' + data.id, {
|
||||
method: 'PUT',
|
||||
body: string
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { List } from '~/types/lists'
|
||||
import type { List, WPListEncrypted } from '~/types/lists'
|
||||
import {useCryptoStore} from '~/composables/crypto'
|
||||
import sodium from "libsodium-wrappers"
|
||||
|
||||
|
||||
const crypto = useCryptoStore()
|
||||
|
||||
export const useListStore = defineStore('lists', {
|
||||
state: () => ({
|
||||
lists: [] as List[],
|
||||
loading: false as boolean,
|
||||
masterKey: null as Uint8Array | null,
|
||||
publicKey: new Uint8Array() as Uint8Array,
|
||||
privateKey: new Uint8Array() as Uint8Array,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
@@ -15,8 +22,11 @@ export const useListStore = defineStore('lists', {
|
||||
resetLists(){
|
||||
this.lists = []
|
||||
},
|
||||
setMasterKey(key: Uint8Array){
|
||||
this.masterKey = key
|
||||
},
|
||||
|
||||
async fetchLists() {
|
||||
async fetchLists() {
|
||||
// On récupère notre plugin API injecté
|
||||
const { $api } = useNuxtApp();
|
||||
this.loading = true;
|
||||
@@ -33,7 +43,107 @@ export const useListStore = defineStore('lists', {
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async processRawData(rawData: [WPListEncrypted]){
|
||||
const { $api } = useNuxtApp();
|
||||
await crypto.init()
|
||||
|
||||
if (rawData && rawData.length > 0) {
|
||||
const item = rawData[0];
|
||||
if (item && this.masterKey){
|
||||
if (this.isWPListEncrypted(item)) {
|
||||
if (item.content_cipher === "initialize-me"){
|
||||
/* 1- Création de la paire de clés (TODO : à déporter ailleurs (Key Store ?)) */
|
||||
const { publicKey, privateKey } = crypto.generateUserKeyPair()
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
|
||||
// On les envoie au BO //
|
||||
/* Chiffrage de la private avec la master : */
|
||||
const privKeyCipher = crypto.encryptPrivateKey(this.privateKey, this.masterKey)
|
||||
|
||||
/* empaquetage */
|
||||
const payload = {
|
||||
'user_id': item.user_id, // pour vérification au BO
|
||||
'private_key_cipher': privKeyCipher,
|
||||
'public_key': publicKey,
|
||||
}
|
||||
|
||||
/* Et on envoie ! */
|
||||
await $api.lists.uploadKeys(JSON.stringify(payload))
|
||||
|
||||
// On crée la liste //
|
||||
/* D'abord la clé AES de la liste */
|
||||
const key = crypto.generateListKey();
|
||||
|
||||
/* Puis la liste */
|
||||
const user_id = Number(item.user_id)
|
||||
|
||||
const data: List = {
|
||||
id: Number(item.id),
|
||||
user_id: user_id,
|
||||
aesKey: key,
|
||||
list_title: 'THE VERY première liste',
|
||||
list_type: 'basic',
|
||||
content: '{[\'vide\']}',
|
||||
is_open: true,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
}
|
||||
this.lists[0] = data
|
||||
|
||||
/* Et on envoie au BO */
|
||||
this.syncData(data, this.publicKey, this.privateKey)
|
||||
}
|
||||
else{
|
||||
console.log('coucou : ' + rawData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async syncData(decryptedData: List, userPublicKey: Uint8Array, userPrivateKey: Uint8Array) {
|
||||
const { $api } = useNuxtApp();
|
||||
|
||||
const aesKey = decryptedData.aesKey;
|
||||
await crypto.init();
|
||||
|
||||
// 1. On prépare les données (en excluant la clé elle-même du contenu)
|
||||
const { aesKey: _, ...pureData } = decryptedData;
|
||||
const stringData = JSON.stringify(pureData);
|
||||
|
||||
// 2. Chiffrement du contenu par la clé AES (XChaCha20)
|
||||
const encryptedContent = crypto.encryptList(stringData, aesKey);
|
||||
|
||||
// 3. Chiffrement de la clé AES par la clé Publique (Asymétrique)
|
||||
// On utilise ici ta fonction de l'étape 11
|
||||
const encryptedKeyPayload = crypto.encryptListKeyForUser(
|
||||
aesKey,
|
||||
userPublicKey,
|
||||
userPrivateKey
|
||||
);
|
||||
|
||||
// 4. On empaquette le tout pour le Back-Office
|
||||
const dataToBO: WPListEncrypted = {
|
||||
id: decryptedData.id.toString(),
|
||||
user_id: decryptedData.user_id.toString(),
|
||||
|
||||
// La clé AES verrouillée pour l'utilisateur
|
||||
key_cipher: sodium.to_base64(encryptedKeyPayload.cipher),
|
||||
key_nonce: sodium.to_base64(encryptedKeyPayload.nonce),
|
||||
|
||||
// Le contenu de la liste
|
||||
content_cipher: sodium.to_base64(encryptedContent.cipher),
|
||||
content_nonce: sodium.to_base64(encryptedContent.nonce),
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
};
|
||||
|
||||
await $api.lists.update(dataToBO)
|
||||
},
|
||||
|
||||
// async updateList(id, title, content) {
|
||||
// const config = useRuntimeConfig();
|
||||
@@ -57,6 +167,26 @@ export const useListStore = defineStore('lists', {
|
||||
// console.error("Erreur lors de la récupération des listes:", error);
|
||||
// }
|
||||
// }
|
||||
|
||||
isWPListEncrypted(obj: any): obj is WPListEncrypted {
|
||||
console.log(obj !== null)
|
||||
console.log(typeof obj)
|
||||
console.log(typeof obj.id)
|
||||
console.log(typeof obj.key_cipher )
|
||||
console.log(typeof obj.key_nonce )
|
||||
console.log(typeof obj.content_cipher)
|
||||
console.log(typeof obj.content_nonce)
|
||||
|
||||
return (
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
typeof obj.id === 'string' &&
|
||||
typeof obj.key_cipher === 'string' &&
|
||||
typeof obj.key_nonce === 'string' &&
|
||||
typeof obj.content_cipher === 'string' &&
|
||||
typeof obj.content_nonce === 'string'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
@@ -1,11 +1,24 @@
|
||||
export interface WPListEncrypted {
|
||||
id: string;
|
||||
user_id: string;
|
||||
key_cipher: string; // La "data" est cachée là-dedans sous forme de string chiffrée
|
||||
key_nonce: string;
|
||||
content_cipher: string;
|
||||
content_nonce: string;
|
||||
created_at: number | null; // Souvent gardé en clair pour le tri côté front
|
||||
updated_at: number | null;
|
||||
}
|
||||
|
||||
export interface List {
|
||||
id: number;
|
||||
user_id: number;
|
||||
aesKey: Uint8Array;
|
||||
list_title: string;
|
||||
list_type: string;
|
||||
encrypted_content: string; // JSON string
|
||||
content: string; // JSON string
|
||||
is_open: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface taskItem {
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@nuxtjs/i18n": "^10.2.1",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"libsodium-wrappers": "^0.8.2",
|
||||
"nuxt": "^4.2.2",
|
||||
"nuxt-vue3-google-signin": "^0.0.13",
|
||||
"pinia": "^3.0.4",
|
||||
@@ -8736,6 +8737,21 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libsodium": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz",
|
||||
"integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/libsodium-wrappers": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz",
|
||||
"integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"libsodium": "^0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@nuxtjs/i18n": "^10.2.1",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"libsodium-wrappers": "^0.8.2",
|
||||
"nuxt": "^4.2.2",
|
||||
"nuxt-vue3-google-signin": "^0.0.13",
|
||||
"pinia": "^3.0.4",
|
||||
|
||||
Reference in New Issue
Block a user