diff --git a/App.js b/App.js index fb58301..010fdf8 100644 --- a/App.js +++ b/App.js @@ -13,28 +13,59 @@ // limitations under the License. import * as React from "react"; -import {PaperProvider} from "react-native-paper"; import {NavigationContainer} from "@react-navigation/native"; -import {BulletList} from "react-content-loader/native"; -import {SQLiteProvider} from "expo-sqlite"; +import {PaperProvider} from "react-native-paper"; +import {SafeAreaView, Text} from "react-native"; +import ContentLoader, {Circle, Rect} from "react-content-loader/native"; import Toast from "react-native-toast-message"; +import {useMigrations} from "drizzle-orm/expo-sqlite/migrator"; + import Header from "./Header"; import NavigationBar from "./NavigationBar"; -import {migrateDb} from "./TotpDatabase"; +import {db} from "./db/client"; +import migrations from "./drizzle/migrations"; const App = () => { + const {success, error} = useMigrations(db, migrations); + + if (error) { + return ( + + Migration error: {error.message} + + ); + } + + if (!success) { + return ( + + + + + + + + + + + ); + } + return ( - }> - - - -
- - - - - - + + +
+ + + + ); }; export default App; diff --git a/Header.js b/Header.js index 224960a..90a98d1 100644 --- a/Header.js +++ b/Header.js @@ -18,13 +18,13 @@ import {Appbar, Avatar, Menu, Text, TouchableRipple} from "react-native-paper"; import Toast from "react-native-toast-message"; import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage"; import useStore from "./useStorage"; -import useSyncStore from "./useSyncStore"; +import {useAccountSync} from "./useAccountStore"; const {width} = Dimensions.get("window"); const Header = () => { const {userInfo, clearAll} = useStore(); - const syncError = useSyncStore(state => state.syncError); + const {syncError, clearSyncError} = useAccountSync(); const [showLoginPage, setShowLoginPage] = React.useState(false); const [menuVisible, setMenuVisible] = React.useState(false); @@ -42,6 +42,7 @@ const Header = () => { const handleCasdoorLogout = () => { CasdoorLogout(); clearAll(); + clearSyncError(); }; const handleSyncErrorPress = () => { diff --git a/HomePage.js b/HomePage.js index 41ac361..7f81a25 100644 --- a/HomePage.js +++ b/HomePage.js @@ -20,16 +20,19 @@ import {CountdownCircleTimer} from "react-native-countdown-circle-timer"; import {useNetInfo} from "@react-native-community/netinfo"; import {FlashList} from "@shopify/flash-list"; import Toast from "react-native-toast-message"; -import * as SQLite from "expo-sqlite/next"; +import {useLiveQuery} from "drizzle-orm/expo-sqlite"; +import {isNull} from "drizzle-orm"; import SearchBar from "./SearchBar"; import EnterAccountDetails from "./EnterAccountDetails"; import ScanQRCode from "./ScanQRCode"; import EditAccountDetails from "./EditAccountDetails"; import AvatarWithFallback from "./AvatarWithFallback"; -import * as TotpDatabase from "./TotpDatabase"; import useStore from "./useStorage"; -import useSyncStore from "./useSyncStore"; +import * as schema from "./db/schema"; +import {db} from "./db/client"; +import {calculateCountdown, validateSecret} from "./totpUtil"; +import {useAccountSync, useEditAccount, useUpdateAccountToken} from "./useAccountStore"; const {width, height} = Dimensions.get("window"); const REFRESH_INTERVAL = 10000; @@ -41,7 +44,7 @@ export default function HomePage() { const [showOptions, setShowOptions] = useState(false); const [showEnterAccountModal, setShowEnterAccountModal] = useState(false); const [searchQuery, setSearchQuery] = useState(""); - const [accounts, setAccounts] = useState([]); + const {data: accounts} = useLiveQuery(db.select().from(schema.accounts).where(isNull(schema.accounts.deletedAt))); const [filteredData, setFilteredData] = useState(accounts); const [showScanner, setShowScanner] = useState(false); const [showEditAccountModal, setShowEditAccountModal] = useState(false); @@ -53,17 +56,10 @@ export default function HomePage() { const [key, setKey] = useState(0); const swipeableRef = useRef(null); - const db = SQLite.useSQLiteContext(); const {userInfo, serverUrl, token} = useStore(); - const {startSync} = useSyncStore(); - const syncError = useSyncStore(state => state.syncError); - - useEffect(() => { - if (db) { - const subscription = SQLite.addDatabaseChangeListener((event) => {loadAccounts();}); - return () => {if (subscription) {subscription.remove();}}; - } - }, [db]); + const {startSync} = useAccountSync(); + const {updateToken} = useUpdateAccountToken(); + const {setAccount, updateAccount, insertAccount, deleteAccount} = useEditAccount(); useEffect(() => { setCanSync(Boolean(isConnected && userInfo && serverUrl)); @@ -73,27 +69,17 @@ export default function HomePage() { setFilteredData(accounts); }, [accounts]); - useEffect(() => { - loadAccounts(); - }, []); - useEffect(() => { const timer = setInterval(() => { - if (canSync) {startSync(db, userInfo, serverUrl, token);} + if (canSync) {startSync(userInfo, serverUrl, token);} }, REFRESH_INTERVAL); return () => clearInterval(timer); }, [startSync]); - const loadAccounts = async() => { - const loadedAccounts = await TotpDatabase.getAllAccounts(db); - setAccounts(loadedAccounts); - setFilteredData(loadedAccounts); - }; - const onRefresh = async() => { setRefreshing(true); if (canSync) { - await startSync(db, userInfo, serverUrl, token); + const syncError = await startSync(userInfo, serverUrl, token); if (syncError) { Toast.show({ type: "error", @@ -110,19 +96,17 @@ export default function HomePage() { }); } } + setKey(prevKey => prevKey + 1); setRefreshing(false); }; const handleAddAccount = async(accountData) => { setKey(prevKey => prevKey + 1); - await TotpDatabase.insertAccount(db, accountData); + setAccount(accountData); + insertAccount(); closeEnterAccountModal(); }; - const handleDeleteAccount = async(id) => { - await TotpDatabase.deleteAccount(db, id); - }; - const handleEditAccount = (account) => { closeSwipeableMenu(); setEditingAccount(account); @@ -132,7 +116,8 @@ export default function HomePage() { const onAccountEdit = async(newAccountName) => { if (editingAccount) { - await TotpDatabase.updateAccountName(db, editingAccount.id, newAccountName); + setAccount({...editingAccount, accountName: newAccountName, oldAccountName: editingAccount.accountName}); + updateAccount(); setPlaceholder(""); setEditingAccount(null); closeEditAccountModal(); @@ -176,7 +161,7 @@ export default function HomePage() { const handleSearch = (query) => { setSearchQuery(query); setFilteredData(query.trim() !== "" - ? accounts.filter(item => item.accountName.toLowerCase().includes(query.toLowerCase())) + ? accounts && accounts.filter(item => item.accountName.toLowerCase().includes(query.toLowerCase())) : accounts ); }; @@ -205,7 +190,7 @@ export default function HomePage() { handleDeleteAccount(item.id)} + onPress={() => deleteAccount(item.id)} > Delete @@ -242,16 +227,16 @@ export default function HomePage() { key={key} isPlaying={true} duration={30} - initialRemainingTime={TotpDatabase.calculateCountdown()} + initialRemainingTime={calculateCountdown()} colors={["#004777", "#0072A0", "#0099CC", "#FF6600", "#CC3300", "#A30000"]} colorsTime={[30, 24, 18, 12, 6, 0]} size={60} onComplete={() => { - TotpDatabase.updateToken(db, item.id); + updateToken(item.id); return { shouldRepeat: true, delay: 0, - newInitialRemainingTime: TotpDatabase.calculateCountdown(), + newInitialRemainingTime: calculateCountdown(), }; }} strokeWidth={5} @@ -318,7 +303,7 @@ export default function HomePage() { transform: [{translateX: -OFFSET_X}, {translateY: -OFFSET_Y}], }} > - + diff --git a/TotpDatabase.js b/TotpDatabase.js deleted file mode 100644 index ac1d30c..0000000 --- a/TotpDatabase.js +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright 2024 The Casdoor Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import totp from "totp-generator"; -import * as api from "./api"; - -export async function migrateDb(db) { - const DATABASE_VERSION = 1; - const result = await db.getFirstAsync("PRAGMA user_version"); - let currentVersion = result?.user_version ?? 0; - if (currentVersion === DATABASE_VERSION) { - return; - } - - if (currentVersion === 0) { - await db.execAsync(` -PRAGMA journal_mode = 'wal'; -CREATE TABLE accounts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - issuer TEXT, - account_name TEXT NOT NULL, - old_account_name TEXT DEFAULT NULL, - secret TEXT NOT NULL, - token TEXT, - is_deleted INTEGER DEFAULT 0, - last_change_time INTEGER DEFAULT (strftime('%s', 'now')), - last_sync_time INTEGER DEFAULT NULL -); -`); - await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`); - currentVersion = 1; - } - - await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`); -} - -export async function clearDatabase(db) { - try { - await db.execAsync("DELETE FROM accounts"); - await db.execAsync("DELETE FROM sqlite_sequence WHERE name='accounts'"); - await db.execAsync("PRAGMA user_version = 0"); - return true; - } catch (error) { - return false; - } -} - -const generateToken = (secretKey) => { - if (secretKey !== null && secretKey !== undefined && secretKey !== "") { - try { - const token = totp(secretKey); - const tokenWithSpace = token.slice(0, 3) + " " + token.slice(3); - return tokenWithSpace; - } catch (error) { - return "Secret Invalid"; - } - } else { - return "Secret Empty"; - } -}; - -export async function insertAccount(db, account) { - const token = generateToken(account.secretKey); - const currentTime = Math.floor(Date.now() / 1000); - return await db.runAsync( - "INSERT INTO accounts (issuer, account_name, secret, token, last_change_time) VALUES (?, ?, ?, ?, ?)", - account.issuer ?? "", - account.accountName, - account.secretKey, - token ?? "", - currentTime - ); -} - -export async function updateAccountName(db, id, newAccountName) { - const account = await db.getFirstAsync("SELECT * FROM accounts WHERE id = ?", id); - const currentTime = Math.floor(Date.now() / 1000); - - // Only update old_account_name if it's null or if last_sync_time is more recent than last_change_time - if (account.old_account_name === null || (account.last_sync_time && account.last_sync_time > account.last_change_time)) { - await db.runAsync(` - UPDATE accounts - SET account_name = ?, - old_account_name = ?, - last_change_time = ? - WHERE id = ? - `, newAccountName, account.account_name, currentTime, id); - } else { - await db.runAsync(` - UPDATE accounts - SET account_name = ?, - last_change_time = ? - WHERE id = ? - `, newAccountName, currentTime, id); - } -} - -export async function updateAccount(db, account, id) { - const token = generateToken(account.secretKey); - const currentTime = Math.floor(Date.now() / 1000); - const result = await db.runAsync( - "UPDATE accounts SET issuer = ?, account_name = ?, old_account_name = ?, secret = ?, token = ?, last_change_time = ? WHERE id = ?", - account.issuer, - account.accountName, - account.oldAccountName ?? null, - account.secretKey, - token ?? "", - currentTime, - id - ); - - if (result.changes === 0) { - throw new Error(`No account updated for id: ${id}`); - } - return result; -} - -export async function deleteAccount(db, id) { - const currentTime = Math.floor(Date.now() / 1000); - await db.runAsync("UPDATE accounts SET is_deleted = 1, last_change_time = ? WHERE id = ?", currentTime, id); -} - -export async function trueDeleteAccount(db, id) { - return await db.runAsync("DELETE FROM accounts WHERE id = ?", id); -} - -export function updateToken(db, id) { - const result = db.getFirstSync("SELECT secret FROM accounts WHERE id = ?", id); - if (result.secret === null) { - return; - } - const token = generateToken(result.secret); - return db.runSync("UPDATE accounts SET token = ? WHERE id = ?", token, id); -} - -export async function updateTokenForAll(db) { - const accounts = await db.getAllAsync("SELECT * FROM accounts WHERE is_deleted = 0"); - for (const account of accounts) { - const token = generateToken(account.secret); - await db.runAsync("UPDATE accounts SET token = ? WHERE id = ?", token, account.id); - } -} - -export async function getAllAccounts(db) { - const accounts = await db.getAllAsync("SELECT * FROM accounts WHERE is_deleted = 0"); - return accounts.map(account => { - const mappedAccount = { - ...account, - accountName: account.account_name, - }; - return mappedAccount; - }); -} - -async function getLocalAccounts(db) { - const accounts = await db.getAllAsync("SELECT * FROM accounts"); - return accounts.map(account => ({ - id: account.id, - issuer: account.issuer, - accountName: account.account_name, - oldAccountName: account.old_account_name, - secretKey: account.secret, - isDeleted: account.is_deleted === 1, - lastChangeTime: account.last_change_time, - lastSyncTime: account.last_sync_time, - })); -} - -async function updateSyncTimeForAll(db) { - const currentTime = Math.floor(Date.now() / 1000); - await db.runAsync("UPDATE accounts SET last_sync_time = ?", currentTime); -} - -export function calculateCountdown() { - const now = Math.round(new Date().getTime() / 1000.0); - return 30 - (now % 30); -} - -export function validateSecret(secret) { - const base32Regex = /^[A-Z2-7]+=*$/i; - if (!secret || secret.length % 8 !== 0) { - return false; - } - return base32Regex.test(secret); -} - -async function updateLocalDatabase(db, mergedAccounts) { - for (const account of mergedAccounts) { - if (account.id) { - if (account.isDeleted) { - await db.runAsync("DELETE FROM accounts WHERE id = ?", account.id); - } else { - await updateAccount(db, account, account.id); - } - } else { - await insertAccount(db, account); - } - } -} - -function getAccountKey(account) { - return `${account.issuer}:${account.accountName}`; -} - -function mergeAccounts(localAccounts, serverAccounts, serverTimestamp) { - const mergedAccounts = new Map(); - const localAccountKeys = new Map(); - - // Process local accounts - for (const local of localAccounts) { - const key = getAccountKey(local); - mergedAccounts.set(key, { - ...local, - synced: false, - }); - - // Store both current and old account keys for local accounts - localAccountKeys.set(key, local); - if (local.oldAccountName) { - const oldKey = getAccountKey({...local, accountName: local.oldAccountName}); - localAccountKeys.set(oldKey, local); - } - } - - const processedLocalKeys = new Set(); - - // Merge with server accounts - for (const server of serverAccounts) { - const serverKey = getAccountKey(server); - const localAccount = localAccountKeys.get(serverKey); - - if (!localAccount) { - // New account from server - mergedAccounts.set(serverKey, {...server, synced: true}); - } else { - const localKey = getAccountKey(localAccount); - const local = mergedAccounts.get(localKey); - - if (serverTimestamp > local.lastChangeTime) { - // Server has newer changes - mergedAccounts.set(localKey, { - ...server, - id: local.id, - oldAccountName: local.accountName !== server.accountName ? local.accountName : local.oldAccountName, - synced: true, - }); - } else if (local.accountName !== server.accountName) { - // Local name change is newer, update the server account name - mergedAccounts.set(localKey, { - ...local, - oldAccountName: server.accountName, - synced: false, - }); - } - // If local is newer or deleted, keep the local version (already in mergedAccounts) - processedLocalKeys.add(localKey); - } - } - // Handle server-side deletions - for (const [key, local] of mergedAccounts) { - if (!processedLocalKeys.has(key) && local.lastSyncTime && local.lastSyncTime < serverTimestamp) { - // This account was not found on the server and was previously synced - // Mark it as deleted - mergedAccounts.set(key, {...local, isDeleted: true, synced: true}); - } - } - - return Array.from(mergedAccounts.values()); -} - -export async function syncWithCloud(db, userInfo, serverUrl, token) { - const localAccounts = await getLocalAccounts(db); - const {updatedTime, mfaAccounts: serverAccounts} = await api.getMfaAccounts( - serverUrl, - userInfo.owner, - userInfo.name, - token - ); - - const serverTimestamp = Math.floor(new Date(updatedTime).getTime() / 1000); - - const mergedAccounts = mergeAccounts(localAccounts, serverAccounts, serverTimestamp); - await updateLocalDatabase(db, mergedAccounts); - - const accountsToSync = mergedAccounts.filter(account => !account.isDeleted).map(account => ({ - issuer: account.issuer, - accountName: account.accountName, - secretKey: account.secretKey, - })); - - const {status} = await api.updateMfaAccounts( - serverUrl, - userInfo.owner, - userInfo.name, - accountsToSync, - token - ); - - if (status !== "ok") { - throw new Error("Sync failed"); - } - - await updateSyncTimeForAll(db); - await updateTokenForAll(db); -} diff --git a/babel.config.js b/babel.config.js index 0843853..dabd037 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,5 +2,6 @@ module.exports = function(api) { api.cache(true); return { presets: ["babel-preset-expo"], + plugins: [["inline-import", {"extensions": [".sql"]}]], }; }; diff --git a/useSyncStore.js b/db/client.js similarity index 50% rename from useSyncStore.js rename to db/client.js index 513cb74..c5fbc83 100644 --- a/useSyncStore.js +++ b/db/client.js @@ -12,27 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {create} from "zustand"; -import * as TotpDatabase from "./TotpDatabase"; +import {drizzle} from "drizzle-orm/expo-sqlite"; +import {openDatabaseSync} from "expo-sqlite/next"; -const useSyncStore = create((set, get) => ({ - isSyncing: false, - syncError: null, +const expoDb = openDatabaseSync("account.db", {enableChangeListener: true}); - startSync: async(db, userInfo, casdoorServer, token) => { - if (!get().isSyncing) { - set({isSyncing: true, syncError: null}); - try { - await TotpDatabase.syncWithCloud(db, userInfo, casdoorServer, token); - } catch (error) { - set({syncError: error.message}); - } finally { - set({isSyncing: false}); - } - } - }, - - clearSyncError: () => set({syncError: null}), -})); - -export default useSyncStore; +export const db = drizzle(expoDb); diff --git a/db/schema.js b/db/schema.js new file mode 100644 index 0000000..66922f2 --- /dev/null +++ b/db/schema.js @@ -0,0 +1,31 @@ +// Copyright 2024 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {integer, sqliteTable, text, unique} from "drizzle-orm/sqlite-core"; +import {sql} from "drizzle-orm"; + +export const accounts = sqliteTable("accounts", { + id: integer("id", {mode: "number"}).primaryKey({autoIncrement: true}), + accountName: text("account_name").notNull(), + oldAccountName: text("old_account_name").default(null), + secretKey: text("secret").notNull(), + issuer: text("issuer").default(null), + token: text("token"), + deletedAt: integer("deleted_at", {mode: "timestamp_ms"}).default(null), + changedAt: integer("changed_at", {mode: "timestamp_ms"}).default(sql`(CURRENT_TIMESTAMP)`), + syncAt: integer("sync_at", {mode: "timestamp_ms"}).default(null), +}, (accounts) => ({ + unq: unique().on(accounts.accountName, accounts.issuer), +}) +); diff --git a/drizzle.config.js b/drizzle.config.js new file mode 100644 index 0000000..25e7196 --- /dev/null +++ b/drizzle.config.js @@ -0,0 +1,6 @@ +export default { + schema: "./db/schema.js", + out: "./drizzle", + dialect: "sqlite", + driver: "expo", +}; diff --git a/drizzle/0000_smooth_owl.sql b/drizzle/0000_smooth_owl.sql new file mode 100644 index 0000000..8ae5462 --- /dev/null +++ b/drizzle/0000_smooth_owl.sql @@ -0,0 +1,13 @@ +CREATE TABLE `accounts` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `account_name` text NOT NULL, + `old_account_name` text DEFAULT 'null', + `secret` text NOT NULL, + `issuer` text DEFAULT 'null', + `token` text, + `deleted_at` integer DEFAULT 'null', + `changed_at` integer DEFAULT (CURRENT_TIMESTAMP), + `sync_at` integer DEFAULT 'null' +); +--> statement-breakpoint +CREATE UNIQUE INDEX `accounts_account_name_issuer_unique` ON `accounts` (`account_name`,`issuer`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..abef5b4 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,103 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "aaa7b5e3-521e-4c3a-970c-35372e7f05a3", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "account_name": { + "name": "account_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "old_account_name": { + "name": "old_account_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'null'" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'null'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'null'" + }, + "changed_at": { + "name": "changed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "sync_at": { + "name": "sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'null'" + } + }, + "indexes": { + "accounts_account_name_issuer_unique": { + "name": "accounts_account_name_issuer_unique", + "columns": [ + "account_name", + "issuer" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..1377ff9 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1724248639995, + "tag": "0000_smooth_owl", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/migrations.js b/drizzle/migrations.js new file mode 100644 index 0000000..a5dbbb9 --- /dev/null +++ b/drizzle/migrations.js @@ -0,0 +1,11 @@ +// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo + +import journal from "./meta/_journal.json"; +import m0000 from "./0000_smooth_owl.sql"; + +export default { + journal, + migrations: { + m0000, + }, +}; diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 0000000..15712fc --- /dev/null +++ b/metro.config.js @@ -0,0 +1,8 @@ +const {getDefaultConfig} = require("expo/metro-config"); + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname); + +config.resolver.sourceExts.push("sql"); + +module.exports = config; diff --git a/package-lock.json b/package-lock.json index 1d9af7c..a4efa0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,14 @@ "@react-navigation/native": "^6.1.7", "@shopify/flash-list": "1.6.4", "casdoor-react-native-sdk": "1.1.0", + "drizzle-orm": "^0.33.0", "eslint-plugin-import": "^2.28.1", "expo": "~51.0.26", "expo-camera": "~15.0.14", "expo-dev-client": "~4.0.22", + "expo-drizzle-studio-plugin": "^0.0.2", "expo-image": "^1.12.13", - "expo-sqlite": "~14.0.6", + "expo-sqlite": "^14.0.6", "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", "expo-updates": "~0.25.22", @@ -49,6 +51,8 @@ "@babel/preset-react": "^7.18.6", "@types/react": "~18.2.79", "@typescript-eslint/eslint-plugin": "^5.62.0", + "babel-plugin-inline-import": "^3.0.0", + "drizzle-kit": "^0.24.0", "eslint": "8.22.0", "eslint-import-resolver-babel-module": "^5.3.2", "eslint-plugin-react": "^7.31.1", @@ -2272,6 +2276,13 @@ "node": ">=0.10.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.8.2.tgz", + "integrity": "sha512-zTrFENsqGvOkBOuHDC1pXCkDXNd2UhP4lI3gYGhQ1R1SPeAAfqzPsV1dcpMy4uNU6kB5VpU5NGhvwxVNETR02A==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -2280,7 +2291,832 @@ "@types/hammerjs": "^2.0.36" }, "engines": { - "node": ">=0.8.0" + "node": ">=0.8.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, "node_modules/@eslint-community/eslint-utils": { @@ -7074,6 +7910,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/babel-plugin-inline-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-inline-import/-/babel-plugin-inline-import-3.0.0.tgz", + "integrity": "sha512-thnykl4FMb8QjMjVCuZoUmAM7r2mnTn5qJwrryCvDv6rugbJlTHZMctdjDtEgD0WBAXJOLJSGXN3loooEwx7UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-resolve": "0.0.2" + } + }, "node_modules/babel-plugin-module-resolver": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.0.tgz", @@ -8284,6 +9130,143 @@ "url": "https://dotenvx.com" } }, + "node_modules/drizzle-kit": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.24.0.tgz", + "integrity": "sha512-rUl5Rf5HLOVkAwHEVEi8xgulIRWzoys0q77RHGCxv5e9v8AI3JGFg7Ug5K1kn513RwNZbuNJMUKOXo0j8kPRgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.8.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.33.0.tgz", + "integrity": "sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.1.1", + "@libsql/client": "*", + "@neondatabase/serverless": ">=0.1", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=13.2.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8520,6 +9503,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -9287,6 +10322,18 @@ "node": ">=10" } }, + "node_modules/expo-drizzle-studio-plugin": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/expo-drizzle-studio-plugin/-/expo-drizzle-studio-plugin-0.0.2.tgz", + "integrity": "sha512-Q5PODIlRp1Hox8a55kJ0iEaaDBL1xo9+oidO03dRu45sB4GAulFcKqc4bZ3qpND4mex0sFu+xm72pmXa/GkjHw==", + "license": "MIT", + "dependencies": { + "expo-sqlite": "^14.0.3" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-eas-client": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-0.12.0.tgz", @@ -10073,6 +11120,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", + "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getenv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz", @@ -13470,6 +14530,13 @@ "node": ">=8" } }, + "node_modules/path-extra": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/path-extra/-/path-extra-1.0.3.tgz", + "integrity": "sha512-vYm3+GCkjUlT1rDvZnDVhNLXIRvwFPaN8ebHAFcuMJM/H0RBOPD7JrcldiNLd9AS3dhAyUHLa4Hny5wp1A+Ffw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -14726,6 +15793,16 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, + "node_modules/require-resolve": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/require-resolve/-/require-resolve-0.0.2.tgz", + "integrity": "sha512-eafQVaxdQsWUB8HybwognkdcIdKdQdQBwTxH48FuE6WI0owZGKp63QYr1MRp73PoX0AcyB7MDapZThYUY8FD0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "x-path": "^0.0.2" + } + }, "node_modules/requireg": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz", @@ -14778,6 +15855,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -16640,6 +17727,16 @@ } } }, + "node_modules/x-path": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/x-path/-/x-path-0.0.2.tgz", + "integrity": "sha512-zQ4WFI0XfJN1uEkkrB19Y4TuXOlHqKSxUJo0Yt+axPjRm8tCG6SJ6+Wo3/+Kjg4c2c8IvBXuJ0uYoshxNn4qMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-extra": "^1.0.2" + } + }, "node_modules/xcode": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", diff --git a/package.json b/package.json index dc96b4b..825c950 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "node_modules/expo/AppEntry.js", "scripts": { - "start": "expo start --tunnel", + "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", @@ -17,12 +17,14 @@ "@react-navigation/native": "^6.1.7", "@shopify/flash-list": "1.6.4", "casdoor-react-native-sdk": "1.1.0", + "drizzle-orm": "^0.33.0", "eslint-plugin-import": "^2.28.1", "expo": "~51.0.26", "expo-camera": "~15.0.14", "expo-dev-client": "~4.0.22", + "expo-drizzle-studio-plugin": "^0.0.2", "expo-image": "^1.12.13", - "expo-sqlite": "~14.0.6", + "expo-sqlite": "^14.0.6", "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", "expo-updates": "~0.25.22", @@ -97,6 +99,8 @@ "@babel/preset-react": "^7.18.6", "@types/react": "~18.2.79", "@typescript-eslint/eslint-plugin": "^5.62.0", + "babel-plugin-inline-import": "^3.0.0", + "drizzle-kit": "^0.24.0", "eslint": "8.22.0", "eslint-import-resolver-babel-module": "^5.3.2", "eslint-plugin-react": "^7.31.1", diff --git a/syncLogic.js b/syncLogic.js new file mode 100644 index 0000000..dfef243 --- /dev/null +++ b/syncLogic.js @@ -0,0 +1,181 @@ +// Copyright 2024 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {eq} from "drizzle-orm"; +import * as schema from "./db/schema"; +import * as api from "./api"; +import {generateToken} from "./totpUtil"; + +function getLocalAccounts(db) { + return db.select().from(schema.accounts).all(); +} + +function getAccountKey(account) { + return `${account.accountName}:${account.issuer ?? ""}`; +} + +async function updateLocalDatabase(db, accounts) { + return db.transaction(async(tx) => { + // remove all accounts + // await tx.delete(schema.accounts).run(); + + for (const account of accounts) { + if (account.id) { + if (account.deletedAt === null || account.deletedAt === undefined) { + // compare all fields + const acc = await tx.select().from(schema.accounts).where(eq(schema.accounts.id, account.id)).get(); + if (acc.issuer === account.issuer && + acc.accountName === account.accountName && + acc.secretKey === account.secretKey && + acc.deletedAt === account.deletedAt + ) { + continue; + } + await tx.update(schema.accounts).set({ + issuer: account.issuer, + accountName: account.accountName, + secretKey: account.secretKey, + deletedAt: null, + token: generateToken(account.secretKey), + changedAt: new Date(), + }).where(eq(schema.accounts.id, account.id)); + } else { + await tx.delete(schema.accounts).where(eq(schema.accounts.id, account.id)); + } + } else { + await tx.insert(schema.accounts).values({ + issuer: account.issuer || null, + accountName: account.accountName, + secretKey: account.secretKey, + token: generateToken(account.secretKey), + }); + } + } + }); +} + +function mergeAccounts(localAccounts, serverAccounts, serverTimestamp) { + const isNewer = (a, b) => new Date(a) > new Date(b); + + const mergedAccounts = new Map(); + const localAccountKeys = new Map(); + + // Process local accounts + for (const local of localAccounts) { + const key = getAccountKey(local); + mergedAccounts.set(key, { + ...local, + synced: false, + }); + + // Store both current and old account keys for local accounts + localAccountKeys.set(key, local); + if (local.oldAccountName) { + const oldKey = getAccountKey({...local, accountName: local.oldAccountName}); + localAccountKeys.set(oldKey, local); + } + } + + const processedLocalKeys = new Set(); + + // Merge with server accounts + for (const server of serverAccounts) { + const serverKey = getAccountKey(server); + const localAccount = localAccountKeys.get(serverKey); + + if (!localAccount) { + // New account from server + mergedAccounts.set(serverKey, {...server, synced: true}); + } else { + const localKey = getAccountKey(localAccount); + const local = mergedAccounts.get(localKey); + + if (isNewer(serverTimestamp, local.changedAt)) { + // Server has newer changes + mergedAccounts.set(localKey, { + ...server, + id: local.id, + oldAccountName: local.accountName !== server.accountName ? local.accountName : local.oldAccountName, + synced: true, + }); + } else if (local.accountName !== server.accountName) { + mergedAccounts.set(localKey, { + ...local, + oldAccountName: server.accountName, + synced: false, + }); + } + // If local is newer or deleted, keep the local version (already in mergedAccounts) + processedLocalKeys.add(localKey); + } + } + + // Handle server-side deletions + for (const [key, local] of mergedAccounts) { + if (!processedLocalKeys.has(key) && local.syncAt && isNewer(serverTimestamp, local.syncAt)) { + // This account was not found on the server and was previously synced + // Mark it as deleted + mergedAccounts.set(key, {...local, deletedAt: new Date(), synced: true}); + } + } + + return Array.from(mergedAccounts.values()); +} + +export async function syncWithCloud(db, userInfo, serverUrl, token) { + // db.delete(schema.accounts).run(); + const localAccounts = await getLocalAccounts(db); + + const {updatedTime, mfaAccounts: serverAccounts} = await api.getMfaAccounts( + serverUrl, + userInfo.owner, + userInfo.name, + token + ); + + const mergedAccounts = mergeAccounts(localAccounts, serverAccounts, updatedTime); + + await updateLocalDatabase(db, mergedAccounts); + + const accountsToSync = mergedAccounts.filter(account => account.deletedAt === null || account.deletedAt === undefined) + .map(account => ({ + issuer: account.issuer, + accountName: account.accountName, + secretKey: account.secretKey, + })); + + const serverAccountsStringified = serverAccounts.map(account => JSON.stringify({ + issuer: account.issuer, + accountName: account.accountName, + secretKey: account.secretKey, + })); + + const accountsToSyncStringified = accountsToSync.map(account => JSON.stringify(account)); + + if (JSON.stringify(accountsToSyncStringified.sort()) !== JSON.stringify(serverAccountsStringified.sort())) { + const {status} = await api.updateMfaAccounts( + serverUrl, + userInfo.owner, + userInfo.name, + accountsToSync, + token + ); + + if (status !== "ok") { + throw new Error("Sync failed"); + } + } + + await db.update(schema.accounts).set({syncAt: new Date()}).run(); +} diff --git a/totpUtil.js b/totpUtil.js new file mode 100644 index 0000000..018b32a --- /dev/null +++ b/totpUtil.js @@ -0,0 +1,42 @@ +// Copyright 2024 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import totp from "totp-generator"; + +export function calculateCountdown(period = 30) { + const now = Math.round(new Date().getTime() / 1000.0); + return period - (now % period); +} + +export function validateSecret(secret) { + const base32Regex = /^[A-Z2-7]+=*$/i; + if (!secret || secret.length % 8 !== 0) { + return false; + } + return base32Regex.test(secret); +} + +export function generateToken(secret) { + if (secret !== null && secret !== undefined && secret !== "") { + try { + const token = totp(secret); + const tokenWithSpace = token.slice(0, 3) + " " + token.slice(3); + return tokenWithSpace; + } catch (error) { + return "Secret Invalid"; + } + } else { + return "Secret Empty"; + } +} diff --git a/useAccountStore.js b/useAccountStore.js new file mode 100644 index 0000000..3a94cfc --- /dev/null +++ b/useAccountStore.js @@ -0,0 +1,115 @@ +// Copyright 2024 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {db} from "./db/client"; +import * as schema from "./db/schema"; +import {eq} from "drizzle-orm"; +import {create} from "zustand"; +import {generateToken} from "./totpUtil"; +import {syncWithCloud} from "./syncLogic"; + +const useEditAccountStore = create((set, get) => ({ + account: {id: undefined, issuer: undefined, accountName: undefined, secretKey: undefined, oldAccountName: undefined}, + setAccount: (account) => set({account}), + updateAccount: () => { + const {id, accountName, issuer, secretKey, oldAccountName} = get().account; + if (!id) {return;} + + const updateData = {}; + if (accountName) {updateData.accountName = accountName;} + if (issuer) {updateData.issuer = issuer;} + if (secretKey) {updateData.secretKey = secretKey;} + + if (Object.keys(updateData).length > 0) { + const currentAccount = db.select().from(schema.accounts) + .where(eq(schema.accounts.id, Number(id))).limit(1) + .get(); + if (currentAccount) { + if (currentAccount.oldAccountName === null && oldAccountName) { + updateData.oldAccountName = oldAccountName; + } + db.update(schema.accounts).set({...updateData, changedAt: new Date()}).where(eq(schema.accounts.id, id)).run(); + } + } + set({ + account: { + id: undefined, + issuer: undefined, + accountName: undefined, + oldAccountName: undefined, + secretKey: undefined, + }, + }); + }, + + insertAccount: () => { + const {accountName, issuer, secretKey} = get().account; + if (!accountName || !secretKey) {return;} + db.insert(schema.accounts) + .values({accountName, issuer: issuer ? issuer : null, secretKey, token: generateToken(secretKey)}) + .run(); + set({account: {id: undefined, issuer: undefined, accountName: undefined, secretKey: undefined}}); + }, + deleteAccount: async(id) => { + db.update(schema.accounts).set({deletedAt: new Date()}).where(eq(schema.accounts.id, id)).run(); + }, +})); + +export const useEditAccount = () => useEditAccountStore(state => ({ + account: state.account, + setAccount: state.setAccount, + updateAccount: state.updateAccount, + insertAccount: state.insertAccount, + deleteAccount: state.deleteAccount, +})); + +const useAccountSyncStore = create((set, get) => ({ + isSyncing: false, + syncError: null, + startSync: async(userInfo, serverUrl, token) => { + if (get().isSyncing) {return;} + + set({isSyncing: true, syncError: null}); + try { + await syncWithCloud(db, userInfo, serverUrl, token); + } catch (error) { + set({syncError: error.message}); + } finally { + set({isSyncing: false}); + } + return get().syncError; + }, + clearSyncError: () => set({syncError: null}), +})); + +export const useAccountSync = () => useAccountSyncStore(state => ({ + isSyncing: state.isSyncing, + syncError: state.syncError, + startSync: state.startSync, + clearSyncError: state.clearSyncError, +})); + +const useUpdateAccountTokenStore = create(() => ({ + updateToken: async(id) => { + const account = db.select().from(schema.accounts) + .where(eq(schema.accounts.id, Number(id))).limit(1).get(); + if (account) { + db.update(schema.accounts).set({token: generateToken(account.secretKey)}).where(eq(schema.accounts.id, id)).run(); + } + }, +})); + +export const useUpdateAccountToken = () => useUpdateAccountTokenStore(state => ({ + updateToken: state.updateToken, +}));