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,
+}));