Migrate PocketBase to new version (#863)

This commit is contained in:
Piotr Rogowski 2022-10-30 13:18:28 +01:00 committed by GitHub
parent 1879c335c3
commit 84e59b10d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 175 additions and 145 deletions

128
package-lock.json generated
View File

@ -20,7 +20,7 @@
"mlg-converter": "^0.7.1", "mlg-converter": "^0.7.1",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"pako": "^2.0.4", "pako": "^2.0.4",
"pocketbase": "^0.7.4", "pocketbase": "^0.8.0-rc1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-ga4": "^1.4.1", "react-ga4": "^1.4.1",
@ -1887,6 +1887,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@gar/promisify": { "node_modules/@gar/promisify": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@ -1895,14 +1907,14 @@
"optional": true "optional": true
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.6", "version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.6.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
"integrity": "sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==", "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@humanwhocodes/object-schema": "^1.2.1", "@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
"minimatch": "^3.0.4" "minimatch": "^3.0.5"
}, },
"engines": { "engines": {
"node": ">=10.10.0" "node": ">=10.10.0"
@ -2461,6 +2473,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@hyper-tuner/eslint-config/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@hyper-tuner/ini": { "node_modules/@hyper-tuner/ini": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://npm.pkg.github.com/download/@hyper-tuner/ini/0.6.2/fbca9ad490d805b93c1ce705ef9d2aee103498e7", "resolved": "https://npm.pkg.github.com/download/@hyper-tuner/ini/0.6.2/fbca9ad490d805b93c1ce705ef9d2aee103498e7",
@ -4107,9 +4131,9 @@
"dev": true "dev": true
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001426", "version": "1.0.30001427",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001427.tgz",
"integrity": "sha512-n7cosrHLl8AWt0wwZw/PJZgUg3lV0gk9LMI7ikGJwhyhgsd2Nb65vKvmSexCqq/J7rbH3mFG6yZZiPR5dLPW5A==", "integrity": "sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -5708,6 +5732,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/eslint/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/espree": { "node_modules/espree": {
"version": "9.4.0", "version": "9.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
@ -8622,9 +8658,9 @@
} }
}, },
"node_modules/pocketbase": { "node_modules/pocketbase": {
"version": "0.7.4", "version": "0.8.0-rc1",
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.7.4.tgz", "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.8.0-rc1.tgz",
"integrity": "sha512-PvBRi4hbgbiBwDjhHa9lGD/ala8dSTjKeNAsHAgsXdIo4v9RgCk2s3Zqd/4UXVBgTJHVM6F7fGOZPvvJfSNVLQ==" "integrity": "sha512-PXY2d0Em639n0WlixAhUtpsUH8At7S2VH0eqPWU0Ouiv7NwJLFFNTIE1IQL9F/cEJZF9BGb6FIYZqb/I4Fj4Iw=="
}, },
"node_modules/pocketbase-typegen": { "node_modules/pocketbase-typegen": {
"version": "1.0.11", "version": "1.0.11",
@ -10511,18 +10547,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/tempy/node_modules/type-fest": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
"integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/terser": { "node_modules/terser": {
"version": "5.15.1", "version": "5.15.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz",
@ -10670,9 +10694,9 @@
} }
}, },
"node_modules/type-fest": { "node_modules/type-fest": {
"version": "0.20.2", "version": "0.16.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -12832,6 +12856,12 @@
"requires": { "requires": {
"type-fest": "^0.20.2" "type-fest": "^0.20.2"
} }
},
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true
} }
} }
}, },
@ -12843,14 +12873,14 @@
"optional": true "optional": true
}, },
"@humanwhocodes/config-array": { "@humanwhocodes/config-array": {
"version": "0.11.6", "version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.6.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
"integrity": "sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==", "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@humanwhocodes/object-schema": "^1.2.1", "@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
"minimatch": "^3.0.4" "minimatch": "^3.0.5"
} }
}, },
"@humanwhocodes/module-importer": { "@humanwhocodes/module-importer": {
@ -13221,6 +13251,12 @@
"requires": { "requires": {
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
} }
},
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true
} }
} }
}, },
@ -14467,9 +14503,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001426", "version": "1.0.30001427",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001427.tgz",
"integrity": "sha512-n7cosrHLl8AWt0wwZw/PJZgUg3lV0gk9LMI7ikGJwhyhgsd2Nb65vKvmSexCqq/J7rbH3mFG6yZZiPR5dLPW5A==", "integrity": "sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ==",
"dev": true "dev": true
}, },
"chalk": { "chalk": {
@ -15294,6 +15330,12 @@
"requires": { "requires": {
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
} }
},
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true
} }
} }
}, },
@ -17642,9 +17684,9 @@
"optional": true "optional": true
}, },
"pocketbase": { "pocketbase": {
"version": "0.7.4", "version": "0.8.0-rc1",
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.7.4.tgz", "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.8.0-rc1.tgz",
"integrity": "sha512-PvBRi4hbgbiBwDjhHa9lGD/ala8dSTjKeNAsHAgsXdIo4v9RgCk2s3Zqd/4UXVBgTJHVM6F7fGOZPvvJfSNVLQ==" "integrity": "sha512-PXY2d0Em639n0WlixAhUtpsUH8At7S2VH0eqPWU0Ouiv7NwJLFFNTIE1IQL9F/cEJZF9BGb6FIYZqb/I4Fj4Iw=="
}, },
"pocketbase-typegen": { "pocketbase-typegen": {
"version": "1.0.11", "version": "1.0.11",
@ -18982,14 +19024,6 @@
"temp-dir": "^2.0.0", "temp-dir": "^2.0.0",
"type-fest": "^0.16.0", "type-fest": "^0.16.0",
"unique-string": "^2.0.0" "unique-string": "^2.0.0"
},
"dependencies": {
"type-fest": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
"integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
"dev": true
}
} }
}, },
"terser": { "terser": {
@ -19111,9 +19145,9 @@
} }
}, },
"type-fest": { "type-fest": {
"version": "0.20.2", "version": "0.16.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
"dev": true "dev": true
}, },
"typescript": { "typescript": {

View File

@ -31,7 +31,7 @@
"mlg-converter": "^0.7.1", "mlg-converter": "^0.7.1",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"pako": "^2.0.4", "pako": "^2.0.4",
"pocketbase": "^0.7.4", "pocketbase": "^0.8.0-rc1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-ga4": "^1.4.1", "react-ga4": "^1.4.1",

View File

@ -10,14 +10,14 @@ export type BaseRecord = {
id: RecordIdString id: RecordIdString
created: IsoDateString created: IsoDateString
updated: IsoDateString updated: IsoDateString
'@collectionId': string collectionId: string
'@collectionName': string collectionName: string
} }
export enum Collections { export enum Collections {
IniFiles = 'iniFiles', IniFiles = 'iniFiles',
Profiles = 'profiles',
Tunes = 'tunes', Tunes = 'tunes',
Users = 'users',
} }
export type IniFilesRecord = { export type IniFilesRecord = {
@ -26,15 +26,8 @@ export type IniFilesRecord = {
ecosystem: 'speeduino' | 'rusefi' ecosystem: 'speeduino' | 'rusefi'
} }
export type ProfilesRecord = {
userId: UserIdString
username: string
avatar?: string
}
export type TunesRecord = { export type TunesRecord = {
user: UserIdString author: RecordIdString
userProfile: RecordIdString
tuneId: string tuneId: string
signature: string signature: string
vehicleName: string vehicleName: string
@ -59,8 +52,12 @@ export type TunesRecord = {
toothLogFiles?: string[] toothLogFiles?: string[]
} }
export type UsersRecord = {
avatar?: string
}
export type CollectionRecords = { export type CollectionRecords = {
iniFiles: IniFilesRecord iniFiles: IniFilesRecord
profiles: ProfilesRecord
tunes: TunesRecord tunes: TunesRecord
users: UsersRecord
} }

View File

@ -9,11 +9,11 @@ import {
import { import {
client, client,
formatError, formatError,
User,
} from '../pocketbase'; } from '../pocketbase';
import { buildRedirectUrl } from '../utils/url'; import { buildRedirectUrl } from '../utils/url';
import { Collections } from '../@types/pocketbase-types'; import { Collections } from '../@types/pocketbase-types';
import { Routes } from '../routes'; import { Routes } from '../routes';
import { UsersRecordFull } from '../types/dbData';
// TODO: this should be imported from pocketbase but currently is not exported // TODO: this should be imported from pocketbase but currently is not exported
export type AuthProviderInfo = { export type AuthProviderInfo = {
@ -39,10 +39,10 @@ export enum OAuthProviders {
}; };
interface AuthValue { interface AuthValue {
currentUser: User | null, currentUser: UsersRecordFull | null,
signUp: (email: string, password: string) => Promise<User>, signUp: (email: string, password: string, username: string) => Promise<UsersRecordFull>,
login: (email: string, password: string) => Promise<User>, login: (email: string, password: string) => Promise<UsersRecordFull>,
refreshUser: () => Promise<User | null>, refreshUser: () => Promise<UsersRecordFull | null>,
sendEmailVerification: () => Promise<void>, sendEmailVerification: () => Promise<void>,
confirmEmailVerification: (token: string) => Promise<void>, confirmEmailVerification: (token: string) => Promise<void>,
confirmResetPassword: (token: string, password: string) => Promise<void>, confirmResetPassword: (token: string, password: string) => Promise<void>,
@ -57,21 +57,24 @@ const AuthContext = createContext<AuthValue | null>(null);
const useAuth = () => useContext<AuthValue>(AuthContext as any); const useAuth = () => useContext<AuthValue>(AuthContext as any);
const users = client.collection(Collections.Users);
const AuthProvider = (props: { children: ReactNode }) => { const AuthProvider = (props: { children: ReactNode }) => {
const { children } = props; const { children } = props;
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<UsersRecordFull | null>(null);
const value = useMemo(() => ({ const value = useMemo(() => ({
currentUser, currentUser,
signUp: async (email: string, password: string) => { signUp: async (email: string, password: string, username: string) => {
try { try {
const user = await client.users.create({ const user = await users.create({
email, email,
password, password,
passwordConfirm: password, passwordConfirm: password,
username,
}); });
client.users.requestVerification(user.email); users.requestVerification(email);
await client.users.authViaEmail(user.email, password); await users.authWithPassword(email, password);
return Promise.resolve(user); return Promise.resolve(user);
} catch (error) { } catch (error) {
@ -80,16 +83,16 @@ const AuthProvider = (props: { children: ReactNode }) => {
}, },
login: async (email: string, password: string) => { login: async (email: string, password: string) => {
try { try {
const authResponse = await client.users.authViaEmail(email, password); const authResponse = await users.authWithPassword(email, password);
return Promise.resolve(authResponse.user); return Promise.resolve(authResponse.record);
} catch (error) { } catch (error) {
return Promise.reject(new Error(formatError(error))); return Promise.reject(new Error(formatError(error)));
} }
}, },
refreshUser: async () => { refreshUser: async () => {
try { try {
const authResponse = await client.users.refresh(); const authResponse = await users.authRefresh();
return Promise.resolve(authResponse.user); return Promise.resolve(authResponse.record);
} catch (error) { } catch (error) {
client.authStore.clear(); client.authStore.clear();
return Promise.resolve(null); return Promise.resolve(null);
@ -97,7 +100,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
}, },
sendEmailVerification: async () => { sendEmailVerification: async () => {
try { try {
await client.users.requestVerification(currentUser!.email); await users.requestVerification(currentUser!.email);
return Promise.resolve(); return Promise.resolve();
} catch (error) { } catch (error) {
return Promise.reject(new Error(formatError(error))); return Promise.reject(new Error(formatError(error)));
@ -105,7 +108,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
}, },
confirmEmailVerification: async (token: string) => { confirmEmailVerification: async (token: string) => {
try { try {
await client.users.confirmVerification(token); await users.confirmVerification(token);
return Promise.resolve(); return Promise.resolve();
} catch (error) { } catch (error) {
return Promise.reject(new Error(formatError(error))); return Promise.reject(new Error(formatError(error)));
@ -113,7 +116,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
}, },
confirmResetPassword: async (token: string, password: string) => { confirmResetPassword: async (token: string, password: string) => {
try { try {
await client.users.confirmPasswordReset(token, password, password); await users.confirmPasswordReset(token, password, password);
return Promise.resolve(); return Promise.resolve();
} catch (error) { } catch (error) {
return Promise.reject(new Error(formatError(error))); return Promise.reject(new Error(formatError(error)));
@ -124,7 +127,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
}, },
initResetPassword: async (email: string) => { initResetPassword: async (email: string) => {
try { try {
await client.users.requestPasswordReset(email); await users.requestPasswordReset(email);
return Promise.resolve(); return Promise.resolve();
} catch (error) { } catch (error) {
return Promise.reject(new Error(formatError(error))); return Promise.reject(new Error(formatError(error)));
@ -132,14 +135,14 @@ const AuthProvider = (props: { children: ReactNode }) => {
}, },
listAuthMethods: async () => { listAuthMethods: async () => {
try { try {
const methods = await client.users.listAuthMethods(); const methods = await users.listAuthMethods();
return Promise.resolve(methods); return Promise.resolve(methods);
} catch (error) { } catch (error) {
return Promise.reject(new Error(formatError(error))); return Promise.reject(new Error(formatError(error)));
} }
}, },
oAuth: async (provider: OAuthProviders, code: string, codeVerifier: string) => { oAuth: async (provider: OAuthProviders, code: string, codeVerifier: string) => {
client.users.authViaOAuth2( users.authWithOAuth2(
provider, provider,
code, code,
codeVerifier, codeVerifier,
@ -148,7 +151,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
}, },
updateUsername: async (username: string) => { updateUsername: async (username: string) => {
try { try {
await client.records.update(Collections.Profiles, currentUser!.profile!.id, { await client.collection(Collections.Users).update(currentUser!.id, {
username, username,
}); });
return Promise.resolve(); return Promise.resolve();
@ -159,24 +162,14 @@ const AuthProvider = (props: { children: ReactNode }) => {
}), [currentUser]); }), [currentUser]);
useEffect(() => { useEffect(() => {
setCurrentUser(client.authStore.model as User | null); setCurrentUser(client.authStore.model as UsersRecordFull | null);
const storeUnsubscribe = client.authStore.onChange((_token, model) => { const storeUnsubscribe = client.authStore.onChange((_token, model) => {
setCurrentUser(model as User | null); setCurrentUser(model as UsersRecordFull | null);
});
client.realtime.subscribe(Collections.Tunes, (event) => {
console.info('Tunes event', event);
});
client.realtime.subscribe(Collections.Profiles, (event) => {
console.info('Profiles event', event);
}); });
return () => { return () => {
storeUnsubscribe(); storeUnsubscribe();
client.realtime.unsubscribe(Collections.Tunes);
client.realtime.unsubscribe(Collections.Profiles);
}; };
}, []); }, []);

View File

@ -15,10 +15,13 @@ import {
TunesRecord, TunesRecord,
} from '../@types/pocketbase-types'; } from '../@types/pocketbase-types';
const tunesCollection = client.collection(Collections.Tunes);
const iniFilesCollection = client.collection(Collections.IniFiles);
const useDb = () => { const useDb = () => {
const updateTune = async (id: string, data: TunesRecordPartial) => { const updateTune = async (id: string, data: TunesRecordPartial) => {
try { try {
await client.records.update(Collections.Tunes, id, data); await tunesCollection.update(id, data);
return Promise.resolve(); return Promise.resolve();
} catch (error) { } catch (error) {
Sentry.captureException(error); Sentry.captureException(error);
@ -30,7 +33,7 @@ const useDb = () => {
const createTune = async (data: TunesRecord) => { const createTune = async (data: TunesRecord) => {
try { try {
const record = await client.records.create(Collections.Tunes, data); const record = await tunesCollection.create(data);
return Promise.resolve(record as TunesRecordFull); return Promise.resolve(record as TunesRecordFull);
} catch (error) { } catch (error) {
@ -43,17 +46,23 @@ const useDb = () => {
const getTune = async (tuneId: string) => { const getTune = async (tuneId: string) => {
try { try {
const tune = await client.records.getList(Collections.Tunes, 1, 1, { const tune = await tunesCollection.getFirstListItem(
filter: `tuneId = "${tuneId}"`, `tuneId = "${tuneId}"`,
expand: 'userProfile', {
}); expand: 'author',
},
);
return Promise.resolve(tune.totalItems > 0 ? tune.items[0] as TunesRecordFull : null); return Promise.resolve(tune as TunesRecordFull);
} catch (error) { } catch (error) {
if ((error as ClientResponseError).isAbort) { if ((error as ClientResponseError).isAbort) {
return Promise.reject(new Error('Cancelled')); return Promise.reject(new Error('Cancelled'));
} }
if ((error as ClientResponseError).status === 404) {
return Promise.resolve(null);
}
Sentry.captureException(error); Sentry.captureException(error);
databaseGenericError(new Error(formatError(error))); databaseGenericError(new Error(formatError(error)));
@ -63,16 +72,18 @@ const useDb = () => {
const getIni = async (signature: string) => { const getIni = async (signature: string) => {
try { try {
const tune = await client.records.getList(Collections.IniFiles, 1, 1, { const ini = await iniFilesCollection.getFirstListItem(`signature = "${signature}"`);
filter: `signature = "${signature}"`,
});
return Promise.resolve(tune.totalItems > 0 ? tune.items[0] as IniFilesRecordFull : null); return Promise.resolve(ini as IniFilesRecordFull);
} catch (error) { } catch (error) {
if ((error as ClientResponseError).isAbort) { if ((error as ClientResponseError).isAbort) {
return Promise.reject(new Error('Cancelled')); return Promise.reject(new Error('Cancelled'));
} }
if ((error as ClientResponseError).status === 404) {
return Promise.resolve(null);
}
Sentry.captureException(error); Sentry.captureException(error);
databaseGenericError(new Error(formatError(error))); databaseGenericError(new Error(formatError(error)));
@ -88,10 +99,10 @@ const useDb = () => {
.join(' || '); .join(' || ');
try { try {
const list = await client.records.getList(Collections.Tunes, page, perPage, { const list = await tunesCollection.getList(page, perPage, {
sort: '-updated', sort: '-updated',
filter, filter,
expand: 'userProfile', expand: 'author',
}); });
return Promise.resolve({ return Promise.resolve({
@ -112,7 +123,7 @@ const useDb = () => {
const autocomplete = async (attribute: string, search: string) => { const autocomplete = async (attribute: string, search: string) => {
try { try {
const items = await client.records.getFullList(Collections.Tunes, 10, { const items = await tunesCollection.getFullList(10, {
filter: `${attribute} ~ "${search}"`, filter: `${attribute} ~ "${search}"`,
}); });

View File

@ -33,10 +33,12 @@ import {
copyToClipboard, copyToClipboard,
isClipboardSupported, isClipboardSupported,
} from '../utils/clipboard'; } from '../utils/clipboard';
import { ProfilesRecord } from '../@types/pocketbase-types';
import { isEscape } from '../utils/keyboard/shortcuts'; import { isEscape } from '../utils/keyboard/shortcuts';
import { TunesRecordFull } from '../types/dbData'; import {
import { formatTime } from '../pocketbase'; TunesRecordFull,
UsersRecordFull,
} from '../types/dbData';
import { formatTime } from '../utils/time';
const { useBreakpoint } = Grid; const { useBreakpoint } = Grid;
const { Text, Title } = Typography; const { Text, Title } = Typography;
@ -64,7 +66,7 @@ const Hub = () => {
...tune, ...tune,
key: tune.tuneId, key: tune.tuneId,
year: tune.year, year: tune.year,
author: (tune['@expand'] as { userProfile: ProfilesRecord }).userProfile.username, author: (tune.expand.author as unknown as UsersRecordFull).username,
displacement: `${tune.displacement}l`, displacement: `${tune.displacement}l`,
aspiration: aspirationMapper[tune.aspiration], aspiration: aspirationMapper[tune.aspiration],
published: formatTime(tune.updated), published: formatTime(tune.updated),

View File

@ -21,7 +21,8 @@ import {
import Loader from '../components/Loader'; import Loader from '../components/Loader';
import { Routes } from '../routes'; import { Routes } from '../routes';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { formatTime } from '../pocketbase'; import { formatTime } from '../utils/time';
import { UsersRecordFull } from '../types/dbData';
const { Item } = Form; const { Item } = Form;
const rowProps = { gutter: 10 }; const rowProps = { gutter: 10 };
@ -39,7 +40,7 @@ const Info = ({ tuneData }: { tuneData: TuneDataState }) => {
tuneId: tuneData.tuneId, tuneId: tuneData.tuneId,
})); }));
const canManage = currentUser && tuneData && currentUser.id === tuneData.user; const canManage = currentUser && tuneData && currentUser.id === tuneData.author;
const manageSection = ( const manageSection = (
<> <>
@ -70,7 +71,7 @@ const Info = ({ tuneData }: { tuneData: TuneDataState }) => {
<Row {...rowProps}> <Row {...rowProps}>
<Col {...colProps}> <Col {...colProps}>
<Item> <Item>
<Input value={tuneData['@expand'].userProfile.username} addonBefore="Author" /> <Input value={(tuneData.expand.author as unknown as UsersRecordFull).username} addonBefore="Author" />
</Item> </Item>
</Col> </Col>
<Col {...colProps}> <Col {...colProps}>

View File

@ -50,7 +50,6 @@ import {
error, error,
restrictedPage, restrictedPage,
signatureNotSupportedWarning, signatureNotSupportedWarning,
usernameNotSet,
} from './auth/notifications'; } from './auth/notifications';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { Routes } from '../routes'; import { Routes } from '../routes';
@ -211,8 +210,7 @@ const UploadPage = () => {
const { signature } = tuneParser.parse(await tuneFile!.arrayBuffer()).getTune().details; const { signature } = tuneParser.parse(await tuneFile!.arrayBuffer()).getTune().details;
const newData: TunesRecord = { const newData: TunesRecord = {
user: currentUser!.id, author: currentUser!.id,
userProfile: currentUser!.profile!.id,
tuneId: newTuneId!, tuneId: newTuneId!,
signature, signature,
vehicleName, vehicleName,
@ -434,7 +432,7 @@ const UploadPage = () => {
if (oldTune) { if (oldTune) {
// this is someone elses tune // this is someone elses tune
if (oldTune.user !== currentUser?.id) { if (oldTune.author !== currentUser?.id) {
navigateToNewTuneId(); navigateToNewTuneId();
return; return;
} }
@ -533,13 +531,6 @@ const UploadPage = () => {
return; return;
} }
if ((user.profile?.username?.length || 0) === 0) {
usernameNotSet();
navigate(Routes.PROFILE);
return;
}
setIsUserAuthorized(true); setIsUserAuthorized(true);
prepareData(); prepareData();
}); });

View File

@ -19,6 +19,7 @@ import {
GithubOutlined, GithubOutlined,
FacebookOutlined, FacebookOutlined,
UserAddOutlined, UserAddOutlined,
UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
Link, Link,
@ -40,6 +41,7 @@ import {
import { import {
emailRules, emailRules,
requiredRules, requiredRules,
usernameRules,
} from '../../utils/form'; } from '../../utils/form';
import { buildRedirectUrl } from '../../utils/url'; import { buildRedirectUrl } from '../../utils/url';
@ -89,10 +91,10 @@ const Login = ({ formRole }: { formRole: FormRoles }) => {
} }
}; };
const emailSignUp = async ({ email, password }: { email: string, password: string }) => { const emailSignUp = async ({ email, password, username }: { email: string, password: string, username: string }) => {
setIsEmailLoading(true); setIsEmailLoading(true);
try { try {
const user = await signUp(email, password); const user = await signUp(email, password, username);
signUpSuccessful(); signUpSuccessful();
if (!user.verified) { if (!user.verified) {
@ -242,6 +244,17 @@ const Login = ({ formRole }: { formRole: FormRoles }) => {
disabled={isAnythingLoading} disabled={isAnythingLoading}
/> />
</Item> </Item>
{!isLogin && <Item
name="username"
rules={usernameRules}
hasFeedback
>
<Input
prefix={<UserOutlined />}
placeholder="Username"
autoComplete="name"
/>
</Item>}
<Item <Item
name="password" name="password"
rules={requiredRules} rules={requiredRules}

View File

@ -106,7 +106,6 @@ const Profile = () => {
</>)} </>)}
<Divider>Your Profile</Divider> <Divider>Your Profile</Divider>
<Space direction="vertical" style={{ width: '100%' }} size="large"> <Space direction="vertical" style={{ width: '100%' }} size="large">
{(currentUser?.profile?.username?.length || 0) === 0 && <Alert message="Remember to set your username!" type="error" showIcon />}
<Form <Form
validateMessages={validateMessages} validateMessages={validateMessages}
form={formProfile} form={formProfile}
@ -114,11 +113,11 @@ const Profile = () => {
fields={[ fields={[
{ {
name: 'username', name: 'username',
value: currentUser?.profile?.username, value: currentUser!.username,
}, },
{ {
name: 'email', name: 'email',
value: currentUser?.email, value: currentUser!.email,
}, },
]} ]}
> >

View File

@ -19,12 +19,6 @@ const emailNotVerified = () => notification.warning({
...baseOptions, ...baseOptions,
}); });
const usernameNotSet = () => notification.warning({
message: 'Update your profile',
description: 'Your username has to be set before you can upload files!',
...baseOptions,
});
const signUpSuccessful = () => notification.success({ const signUpSuccessful = () => notification.success({
message: 'Sign Up successful', message: 'Sign Up successful',
description: 'Welcome on board!', description: 'Welcome on board!',
@ -156,7 +150,6 @@ const downloading = () => notification.success({
export { export {
error, error,
emailNotVerified, emailNotVerified,
usernameNotSet,
signUpSuccessful, signUpSuccessful,
signUpFailed, signUpFailed,
logInSuccessful, logInSuccessful,

View File

@ -1,6 +1,5 @@
import PocketBase, { import PocketBase, {
ClientResponseError, ClientResponseError,
User,
Record, Record,
} from 'pocketbase'; } from 'pocketbase';
import { fetchEnv } from './utils/env'; import { fetchEnv } from './utils/env';
@ -23,16 +22,11 @@ const formatError = (error: any) => {
const removeFilenameSuffix = (filename: string) => filename.replace(/(.+)(_\w{10})(\.\w+)$/, '$1$3'); const removeFilenameSuffix = (filename: string) => filename.replace(/(.+)(_\w{10})(\.\w+)$/, '$1$3');
// NOTE: PocketBase doesn't return ISO time, this may change here: https://github.com/pocketbase/pocketbase/issues/376
const formatTime = (time: string) => new Date(`${time}Z`).toLocaleString();
export { export {
API_URL, API_URL,
client, client,
formatError, formatError,
formatTime,
removeFilenameSuffix, removeFilenameSuffix,
ClientResponseError, ClientResponseError,
User,
Record, Record,
}; };

View File

@ -1,8 +1,8 @@
import { Record } from '../pocketbase'; import { Record } from '../pocketbase';
import { import {
IniFilesRecord, IniFilesRecord,
ProfilesRecord,
TunesRecord, TunesRecord,
UsersRecord,
} from '../@types/pocketbase-types'; } from '../@types/pocketbase-types';
type Partial<T> = { type Partial<T> = {
@ -13,6 +13,6 @@ export type TunesRecordPartial = Partial<TunesRecord>;
export interface TunesRecordFull extends TunesRecord, Record { } export interface TunesRecordFull extends TunesRecord, Record { }
export interface ProfilesRecordFull extends ProfilesRecord, Record { } export interface UsersRecordFull extends UsersRecord, Record { }
export interface IniFilesRecordFull extends IniFilesRecord, Record { } export interface IniFilesRecordFull extends IniFilesRecord, Record { }

2
src/utils/time.ts Normal file
View File

@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export const formatTime = (time: string) => new Date(time).toLocaleString();