diff --git a/package-lock.json b/package-lock.json
index 20ac5e97..6be75bfc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@types/bootstrap": "^5.2.10",
"bootstrap": "^5.3.3",
"electron-log": "^5.1.4",
+ "moment": "^2.30.1",
"vue": "^3.4.21",
"vue-router": "^4.3.2"
},
@@ -3968,6 +3969,15 @@
"ufo": "^1.5.3"
}
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
diff --git a/package.json b/package.json
index e4fdbea0..e2760af6 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@types/bootstrap": "^5.2.10",
"bootstrap": "^5.3.3",
"electron-log": "^5.1.4",
+ "moment": "^2.30.1",
"vue": "^3.4.21",
"vue-router": "^4.3.2"
},
diff --git a/packages/renderer/src/App.vue b/packages/renderer/src/App.vue
index 0b610833..89fccd01 100644
--- a/packages/renderer/src/App.vue
+++ b/packages/renderer/src/App.vue
@@ -7,6 +7,7 @@ import { getDeviceInfo } from './helpers';
import Header from './components/Header.vue';
import ErrorMessage from './components/ErrorMessage.vue';
+import Settings from './components/Settings.vue';
const router = useRouter();
@@ -46,6 +47,13 @@ const showError = (message: string) => {
};
provide('showError', showError);
+// Settings
+const showSettingsModal = ref(false);
+const showSettings = () => {
+ showSettingsModal.value = true;
+};
+provide('showSettings', showSettings);
+
// Navigation
const navigate = (path: string) => {
router.push(path);
@@ -69,8 +77,13 @@ onMounted(async () => {
-
+
+
+
+
+
+
@@ -107,6 +120,15 @@ body {
z-index: 1050;
}
+/* Headers */
+h1 {
+ font-size: 1.5rem;
+}
+
+h2 {
+ font-size: 1.25rem;
+}
+
/* Bootstrap style that for some reason aren't making it */
.mr-1 {
margin-right: 0.25rem;
@@ -119,4 +141,8 @@ body {
.mr-3 {
margin-right: 1rem;
}
+
+.mb-3 {
+ margin-bottom: 1rem;
+}
diff --git a/packages/renderer/src/ServerAPI.test.ts b/packages/renderer/src/ServerAPI.test.ts
index f2cb74d6..179503f2 100644
--- a/packages/renderer/src/ServerAPI.test.ts
+++ b/packages/renderer/src/ServerAPI.test.ts
@@ -190,5 +190,3 @@ test('ServerAPI.getNewApiToken() returns false on invalid device token', async (
expect(result).toBe(false);
});
-// Authentication tests
-
diff --git a/packages/renderer/src/ServerAPI.ts b/packages/renderer/src/ServerAPI.ts
index 0efad446..07865d92 100644
--- a/packages/renderer/src/ServerAPI.ts
+++ b/packages/renderer/src/ServerAPI.ts
@@ -181,6 +181,25 @@ export default class ServerAPI {
}
}
+ async getDevices(): Promise {
+ console.log("GET /devices");
+ if (!await this.validateApiToken()) {
+ return this.returnError("Failed to get a new API token.")
+ }
+ try {
+ const response = await this.fetchAuthenticated("GET", `${this.apiUrl}/devices`, null);
+ if (response.status != 200) {
+ return this.returnError("Failed to get devices. Got status code " + response.status + ".")
+ }
+ const data: GetDevicesApiResponseArray = {
+ devices: await response.json()
+ };
+ return data;
+ } catch {
+ return this.returnError("Failed to get devices. Maybe the server is down?")
+ }
+ }
+
async ping(): Promise {
console.log("GET /ping");
if (!await this.validateApiToken()) {
diff --git a/packages/renderer/src/components/ErrorMessage.vue b/packages/renderer/src/components/ErrorMessage.vue
index 395886da..6a2163cf 100644
--- a/packages/renderer/src/components/ErrorMessage.vue
+++ b/packages/renderer/src/components/ErrorMessage.vue
@@ -3,14 +3,12 @@ import { ref, onMounted, onUnmounted } from 'vue';
import Modal from 'bootstrap/js/dist/modal';
defineProps({
- message: String,
- showErrorMessage: Boolean
+ message: String
});
-const emit = defineEmits(['update:showErrorMessage']);
-
-const hideError = () => {
- emit('update:showErrorMessage', false);
+const emit = defineEmits(['hide']);
+const hide = () => {
+ emit('hide');
};
const errorModal = ref(null);
@@ -24,34 +22,33 @@ onMounted(() => {
// The 'hidden.bs.modal' event is triggered when when the user clicks outside the modal
modalElement.addEventListener('hidden.bs.modal', () => {
- hideError();
+ hide();
});
}
});
onUnmounted(() => {
if (errorModal.value && modalInstance) {
- errorModal.value.removeEventListener('hidden.bs.modal', hideError);
+ errorModal.value.removeEventListener('hidden.bs.modal', hide);
}
});
-
+
diff --git a/packages/renderer/src/components/Header.vue b/packages/renderer/src/components/Header.vue
index 7c5e3e18..bb986319 100644
--- a/packages/renderer/src/components/Header.vue
+++ b/packages/renderer/src/components/Header.vue
@@ -1,18 +1,21 @@
@@ -58,8 +63,9 @@ const signOut = async () => {
{{ userEmail }}
-
-
+
+
diff --git a/packages/renderer/src/components/Settings.vue b/packages/renderer/src/components/Settings.vue
new file mode 100644
index 00000000..46ead712
--- /dev/null
+++ b/packages/renderer/src/components/Settings.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
No devices found. This should never happen, since you're using a device right now.
+
+
+
Active devices
+
+
+
+
+
+ {{ formatDescription(device.description) }}
+
+
+ you are currently using this device
+
+
+
+ last accessed {{
+ relativeTime(device.lastAccessedAt) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/renderer/src/helpers.ts b/packages/renderer/src/helpers.ts
index 919c5e03..f6a0eb3f 100644
--- a/packages/renderer/src/helpers.ts
+++ b/packages/renderer/src/helpers.ts
@@ -7,6 +7,7 @@ export async function getDeviceInfo(): Promise {
"userEmail": "",
"deviceDescription": "",
"deviceToken": "",
+ "deviceUUID": "",
"apiToken": "",
"valid": false
};
@@ -30,6 +31,11 @@ export async function getDeviceInfo(): Promise {
if (deviceToken && deviceToken.length > 0) {
deviceInfo["deviceToken"] = deviceToken;
+ const deviceUUID = await (window as any).electron.getConfig("deviceUUID");
+ if (deviceUUID) {
+ deviceInfo["deviceUUID"] = deviceUUID;
+ }
+
serverApi.setUserEmail(userEmail);
serverApi.setDeviceToken(deviceToken);
const pingResp = await serverApi.ping();
diff --git a/packages/renderer/src/views/LoginView.vue b/packages/renderer/src/views/LoginView.vue
index 90e247c1..1195c27b 100644
--- a/packages/renderer/src/views/LoginView.vue
+++ b/packages/renderer/src/views/LoginView.vue
@@ -89,6 +89,9 @@ async function registerDevice() {
return;
}
+ // Save the device UUID
+ await (window as any).electron.setConfig("deviceUUID", registerDeviceResp.uuid);
+
// Save the device token
await (window as any).electron.setConfig("deviceToken", registerDeviceResp.deviceToken);
serverApi.value.setDeviceToken(registerDeviceResp.deviceToken);
diff --git a/packages/renderer/src/vite-env.d.ts b/packages/renderer/src/vite-env.d.ts
index a64a00c3..f7eb118d 100644
--- a/packages/renderer/src/vite-env.d.ts
+++ b/packages/renderer/src/vite-env.d.ts
@@ -4,6 +4,7 @@ type DeviceInfo = {
userEmail: string;
deviceDescription: string;
deviceToken: string;
+ deviceUUID: string;
apiToken: string;
valid: boolean;
};
@@ -31,9 +32,21 @@ type RegisterDeviceApiRequest = {
};
type RegisterDeviceApiResponse = {
+ uuid: string;
deviceToken: string;
};
+// API models for GET /api/v1/devices (an array of these)
+type GetDevicesApiResponse = {
+ uuid: string;
+ description: string;
+ lastAccessedAt: Date;
+};
+
+type GetDevicesApiResponseArray = {
+ devices: GetDevicesApiResponse[];
+};
+
// API models for POST /api/v1/token
type TokenApiRequest = {
email: string;
@@ -53,5 +66,5 @@ type LogoutApiResponse = {
// API models for DELETE /api/v1/device
type DeleteDeviceApiRequest = {
- deviceToken: string;
+ uuid: string;
};
\ No newline at end of file