Compare commits
No commits in common. "2b1ce51d319857239727977244c002d1dc9cb68a" and "8e8f5b8faf697bf47d10a6eafb8bf3671e94824a" have entirely different histories.
2b1ce51d31
...
8e8f5b8faf
@ -226,14 +226,6 @@
|
|||||||
@apply bg-[var(--dark-background)];
|
@apply bg-[var(--dark-background)];
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item-content {
|
|
||||||
@apply flex flex-col;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item-bottom-row {
|
|
||||||
@apply mt-1 flex items-center justify-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item-text {
|
.sidebar-item-text {
|
||||||
@apply text-xs text-[var(--light-text)]/60;
|
@apply text-xs text-[var(--light-text)]/60;
|
||||||
}
|
}
|
||||||
@ -242,30 +234,6 @@
|
|||||||
@apply text-[var(--dark-text)]/60;
|
@apply text-[var(--dark-text)]/60;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item-delete {
|
|
||||||
@apply flex h-6.5 w-6.5 items-center justify-center rounded-lg border-1 border-[var(--light-accent)]/20 bg-transparent p-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sidebar-item-delete {
|
|
||||||
@apply border-[var(--dark-accent)]/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item-delete > svg {
|
|
||||||
@apply text-[var(--light-accent)]/50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sidebar-item-delete > svg {
|
|
||||||
@apply text-[var(--dark-accent)]/50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item-delete:hover > svg {
|
|
||||||
@apply text-[var(--light-accent)] transition-colors delay-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sidebar-item-delete:hover > svg {
|
|
||||||
@apply text-[var(--dark-accent)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-divider {
|
.sidebar-divider {
|
||||||
@apply divide-y divide-[var(--light-text)]/20;
|
@apply divide-y divide-[var(--light-text)]/20;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ export const cError: Writable<string | null> = writable(null)
|
|||||||
class ApiClient {
|
class ApiClient {
|
||||||
private viewCookieName: string
|
private viewCookieName: string
|
||||||
private baseUrl: string
|
private baseUrl: string
|
||||||
private lastAtUpdate = new Date(0) // Refreshing the page wipes access and CSRF tokens from memory -> Rotation needed
|
private lastAtUpdate = new Date(0)
|
||||||
private lastCsrfUpdate = new Date(0)
|
private lastCsrfUpdate = new Date(0)
|
||||||
private refreshInProgress = false
|
private refreshInProgress = false
|
||||||
private activeVersion = -1
|
private activeVersion = -1
|
||||||
@ -123,7 +123,7 @@ class ApiClient {
|
|||||||
private async handleRequest<T>(
|
private async handleRequest<T>(
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
options: { useBearerAuth: boolean }
|
options: { useBearerAuth: boolean }
|
||||||
): Promise<T | null> {
|
): Promise<T | undefined> {
|
||||||
isPending.set(true)
|
isPending.set(true)
|
||||||
cError.set(null)
|
cError.set(null)
|
||||||
|
|
||||||
@ -137,21 +137,18 @@ class ApiClient {
|
|||||||
try {
|
try {
|
||||||
await this.checkAndRefreshAccessToken()
|
await this.checkAndRefreshAccessToken()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[REQ] Refresh attempt not successful")
|
console.log("refresh attempt not successful")
|
||||||
await this.handleLocalLogout()
|
await this.handleLocalLogout()
|
||||||
throw new Error("Session expired, please authenticate again.")
|
throw new Error("Session expired, please authenticate again.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await fn()
|
return await fn()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
cError.set(err instanceof Error ? err.message : "Unknown error")
|
cError.set(err instanceof Error ? err.message : "Unknown error")
|
||||||
console.log(`[ERR] ${get(cError)}`)
|
console.log(`error: ${get(cError)}`)
|
||||||
} finally {
|
} finally {
|
||||||
isPending.set(false)
|
isPending.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be attached to routes that handle authentication with the bearer token (access token)
|
// Should be attached to routes that handle authentication with the bearer token (access token)
|
||||||
@ -164,12 +161,12 @@ class ApiClient {
|
|||||||
// This should never happen due to the token expiration checks we make client-side,
|
// This should never happen due to the token expiration checks we make client-side,
|
||||||
// but it's still good to have as a fallback
|
// but it's still good to have as a fallback
|
||||||
try {
|
try {
|
||||||
console.log("[RES] Unexpected 401 caught, attempting to refresh...")
|
console.log("unexpected 401 caught, attempting refresh")
|
||||||
await this.checkAndRefreshAccessToken()
|
await this.checkAndRefreshAccessToken()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = err instanceof Error ? err.message : "Unknown error"
|
console.log("refresh attempt not successful")
|
||||||
await this.handleLocalLogout()
|
await this.handleLocalLogout()
|
||||||
throw new Error(errMsg)
|
throw new Error("Session expired, please authenticate again.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,16 +192,20 @@ class ApiClient {
|
|||||||
const timeSinceUpdate = Date.now() - this.lastAtUpdate.getTime()
|
const timeSinceUpdate = Date.now() - this.lastAtUpdate.getTime()
|
||||||
const needsRefresh = timeSinceUpdate > AT_EXP_MS - REFRESH_BUF
|
const needsRefresh = timeSinceUpdate > AT_EXP_MS - REFRESH_BUF
|
||||||
|
|
||||||
|
console.log(`timeSinceUpdate: ${timeSinceUpdate}`)
|
||||||
|
|
||||||
if (needsRefresh) {
|
if (needsRefresh) {
|
||||||
console.log(`[AUTH] Running token refresh attempt (timeSinceUpdate=${timeSinceUpdate})`)
|
console.log("running token refresh attempt")
|
||||||
|
|
||||||
this.refreshInProgress = true
|
this.refreshInProgress = true
|
||||||
await this.refreshAccessToken()
|
await this.refreshAccessToken()
|
||||||
this.refreshInProgress = false
|
this.refreshInProgress = false
|
||||||
|
} else {
|
||||||
|
console.log("no need to rotate tokens")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshAccessToken(): Promise<void | null> {
|
private async refreshAccessToken(): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/auth/cookie/refresh`, {
|
const response = await fetch(`${this.baseUrl}/auth/cookie/refresh`, {
|
||||||
@ -234,7 +235,7 @@ class ApiClient {
|
|||||||
const needsRefresh = timeSinceUpdate > CSRF_EXP_MS - REFRESH_BUF
|
const needsRefresh = timeSinceUpdate > CSRF_EXP_MS - REFRESH_BUF
|
||||||
|
|
||||||
if (!token || needsRefresh) {
|
if (!token || needsRefresh) {
|
||||||
console.log("[AUTH] Refreshing CSRF token")
|
console.log("refreshing csrf token")
|
||||||
await this.refreshCsrfToken()
|
await this.refreshCsrfToken()
|
||||||
token = get(csrfToken)
|
token = get(csrfToken)
|
||||||
}
|
}
|
||||||
@ -242,7 +243,7 @@ class ApiClient {
|
|||||||
return { "X-Csrf-Token": token || "" }
|
return { "X-Csrf-Token": token || "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshCsrfToken(): Promise<void | null> {
|
private async refreshCsrfToken(): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/auth/cookie/csrf`, {
|
const response = await fetch(`${this.baseUrl}/auth/cookie/csrf`, {
|
||||||
@ -262,7 +263,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleLocalLogout(): Promise<void | null> {
|
private async handleLocalLogout(): Promise<void> {
|
||||||
this.lastAtUpdate = new Date(0)
|
this.lastAtUpdate = new Date(0)
|
||||||
this.lastCsrfUpdate = new Date(0)
|
this.lastCsrfUpdate = new Date(0)
|
||||||
|
|
||||||
@ -285,7 +286,7 @@ class ApiClient {
|
|||||||
const viewCookie = this.getCookieValue(this.viewCookieName)
|
const viewCookie = this.getCookieValue(this.viewCookieName)
|
||||||
|
|
||||||
if (!viewCookie) {
|
if (!viewCookie) {
|
||||||
console.log("[AUTH] View cookie not found")
|
console.log("view cookie not found")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,11 +294,14 @@ class ApiClient {
|
|||||||
const expirationTimestamp = parseInt(value, 10)
|
const expirationTimestamp = parseInt(value, 10)
|
||||||
|
|
||||||
if (isNaN(expirationTimestamp)) {
|
if (isNaN(expirationTimestamp)) {
|
||||||
console.log(`[ERR] Invalid view cookie expiration timestamp: ${value}`)
|
console.log(`invalid expiration timestamp: ${value}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return now < new Date(expirationTimestamp * 1000)
|
const expirationDate = new Date(expirationTimestamp * 1000)
|
||||||
|
console.log(`auth cookie expiration: ${expirationDate}`)
|
||||||
|
|
||||||
|
return now < expirationDate
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteViewCookie() {
|
private deleteViewCookie() {
|
||||||
@ -374,12 +378,12 @@ class ApiClient {
|
|||||||
private joinDeserializedVersion(
|
private joinDeserializedVersion(
|
||||||
noteID: string,
|
noteID: string,
|
||||||
apiResponse: ApiFullVersionResponse
|
apiResponse: ApiFullVersionResponse
|
||||||
): FullNote | null {
|
): FullNote | undefined {
|
||||||
// Cache lookups are safe here due to this always being called *after* fetching the actual `FullNote`
|
// Cache lookups are safe here due to this always being called *after* fetching the actual `FullNote`
|
||||||
const cachedNote = this.loadedNotesCache.get(noteID)
|
const cachedNote = this.loadedNotesCache.get(noteID)
|
||||||
|
|
||||||
if (!cachedNote) {
|
if (!cachedNote) {
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -395,7 +399,7 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async register(username: string, password: string): Promise<void | null> {
|
public async register(username: string, password: string): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/auth/signup`, {
|
const response = await fetch(`${this.baseUrl}/auth/signup`, {
|
||||||
@ -404,21 +408,20 @@ class ApiClient {
|
|||||||
body: JSON.stringify({ username, password })
|
body: JSON.stringify({ username, password })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Can't overwrite the function parameter
|
// Can't overwrite `username` parameter
|
||||||
const data = await this.handleResponse<{
|
const data = await this.handleResponse<{
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
}>(response, { useBearerAuth: false })
|
}>(response, { useBearerAuth: false })
|
||||||
|
|
||||||
console.log(`[USER] Registration of user '${data.username}' successful`)
|
console.log(`${data.username} -> ${data.id}`)
|
||||||
|
|
||||||
await goto("/login")
|
await goto("/login")
|
||||||
},
|
},
|
||||||
{ useBearerAuth: false }
|
{ useBearerAuth: false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(username: string, password: string): Promise<void | null> {
|
public async login(username: string, password: string): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/auth/login`, {
|
const response = await fetch(`${this.baseUrl}/auth/login`, {
|
||||||
@ -441,7 +444,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(): Promise<void | null> {
|
public async logout(): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/auth/logout`, {
|
const response = await fetch(`${this.baseUrl}/auth/logout`, {
|
||||||
@ -453,7 +456,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
console.log("[USER] Logout successful")
|
console.log("logout successful")
|
||||||
await this.handleLocalLogout()
|
await this.handleLocalLogout()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -465,7 +468,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getCurrentUser(): Promise<void | null> {
|
public async getCurrentUser(): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/auth/me`, {
|
const response = await fetch(`${this.baseUrl}/auth/me`, {
|
||||||
@ -476,16 +479,15 @@ class ApiClient {
|
|||||||
const data = await this.handleResponse<ApiUserResponse>(response, { useBearerAuth: false })
|
const data = await this.handleResponse<ApiUserResponse>(response, { useBearerAuth: false })
|
||||||
const user = this.deserializeUser(data)
|
const user = this.deserializeUser(data)
|
||||||
|
|
||||||
|
console.log(user)
|
||||||
|
|
||||||
currentUser.set(user)
|
currentUser.set(user)
|
||||||
},
|
},
|
||||||
{ useBearerAuth: true }
|
{ useBearerAuth: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateCurrentUserPassword(
|
public async updateCurrentUserPassword(oldPassword: string, newPassword: string): Promise<void> {
|
||||||
oldPassword: string,
|
|
||||||
newPassword: string
|
|
||||||
): Promise<void | null> {
|
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const data = {
|
const data = {
|
||||||
@ -500,21 +502,21 @@ class ApiClient {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { accessToken: token, user } = await this.handleResponse<{
|
const { accessToken: token, user } = await this.handleResponse<{
|
||||||
accessToken: string
|
accessToken: string
|
||||||
user: User
|
user: User
|
||||||
}>(response, { useBearerAuth: false })
|
}>(response, { useBearerAuth: false })
|
||||||
|
|
||||||
accessToken.set(token)
|
accessToken.set(token)
|
||||||
currentUser.set(user || null)
|
currentUser.set(user || null)
|
||||||
this.lastAtUpdate = new Date()
|
this.lastAtUpdate = new Date()
|
||||||
|
|
||||||
|
console.log(user)
|
||||||
},
|
},
|
||||||
{ useBearerAuth: true }
|
{ useBearerAuth: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteCurrentUser(password: string): Promise<void | null> {
|
public async deleteCurrentUser(password: string): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/auth/owner`, {
|
const response = await fetch(`${this.baseUrl}/auth/owner`, {
|
||||||
@ -527,7 +529,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
console.log("[USER] Deletion successful")
|
console.log("deletion successful")
|
||||||
await this.handleLocalLogout()
|
await this.handleLocalLogout()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -539,7 +541,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async adminListAll(): Promise<User[] | null> {
|
public async adminListAll(): Promise<User[] | undefined> {
|
||||||
const user = get(currentUser)
|
const user = get(currentUser)
|
||||||
if (!user || !user.isAdmin) {
|
if (!user || !user.isAdmin) {
|
||||||
throw new Error("Admin privileges required.")
|
throw new Error("Admin privileges required.")
|
||||||
@ -557,7 +559,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const users = await this.handleResponse<User[]>(response, { useBearerAuth: false })
|
const users = await this.handleResponse<User[]>(response, { useBearerAuth: false })
|
||||||
console.log(`[ADMIN] Got ${users.length} user results`)
|
console.log(`admin: got ${users.length} user results`)
|
||||||
|
|
||||||
return users
|
return users
|
||||||
},
|
},
|
||||||
@ -565,7 +567,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async adminDeleteUser(userID: string): Promise<void | null> {
|
public async adminDeleteUser(userID: string): Promise<void> {
|
||||||
const user = get(currentUser)
|
const user = get(currentUser)
|
||||||
if (!user || !user.isAdmin) {
|
if (!user || !user.isAdmin) {
|
||||||
throw new Error("Admin privileges required.")
|
throw new Error("Admin privileges required.")
|
||||||
@ -585,7 +587,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
console.log(`[ADMIN] Deletion of user '${userID}' successful`)
|
console.log("admin: deletion successful")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -595,7 +597,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listNotes(): Promise<NoteMetadata[] | null> {
|
public async listNotes(): Promise<NoteMetadata[] | undefined> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@ -616,7 +618,7 @@ class ApiClient {
|
|||||||
notes = this.deserializeNoteMetadatas(data)
|
notes = this.deserializeNoteMetadatas(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[NOTE] Got ${notes.length} note metadata results`)
|
console.log(`got ${notes.length} note metadata results`)
|
||||||
|
|
||||||
return notes
|
return notes
|
||||||
},
|
},
|
||||||
@ -624,7 +626,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createNote(): Promise<NewNoteResponse | null> {
|
public async createNote(): Promise<NewNoteResponse | undefined> {
|
||||||
// NOTE: The initial note version doesn't allow any user input, the first user-made modification
|
// NOTE: The initial note version doesn't allow any user input, the first user-made modification
|
||||||
// is applied through the version creation endpoint
|
// is applied through the version creation endpoint
|
||||||
|
|
||||||
@ -642,7 +644,10 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getActiveFullNote(noteID: string, fetchRemote: boolean): Promise<FullNote | null> {
|
public async getActiveFullNote(
|
||||||
|
noteID: string,
|
||||||
|
fetchRemote: boolean
|
||||||
|
): Promise<FullNote | undefined> {
|
||||||
if (!UUID_REGEX.test(noteID)) {
|
if (!UUID_REGEX.test(noteID)) {
|
||||||
throw new Error("Invalid note ID format.")
|
throw new Error("Invalid note ID format.")
|
||||||
}
|
}
|
||||||
@ -651,6 +656,7 @@ class ApiClient {
|
|||||||
if (!fetchRemote) {
|
if (!fetchRemote) {
|
||||||
const cachedNote = this.loadedNotesCache.get(noteID)
|
const cachedNote = this.loadedNotesCache.get(noteID)
|
||||||
if (cachedNote != null) {
|
if (cachedNote != null) {
|
||||||
|
// console.log(`full note cache hit ${noteID}`)
|
||||||
this.activeVersion = cachedNote.versionNumber
|
this.activeVersion = cachedNote.versionNumber
|
||||||
return cachedNote
|
return cachedNote
|
||||||
}
|
}
|
||||||
@ -669,7 +675,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
const note = this.deserializeFullNote(data)
|
const note = this.deserializeFullNote(data)
|
||||||
|
|
||||||
console.log(`[CACHE] Storing ${noteID}`)
|
console.log(`caching ${noteID}`)
|
||||||
this.loadedNotesCache.set(noteID, note)
|
this.loadedNotesCache.set(noteID, note)
|
||||||
this.activeVersion = note.versionNumber
|
this.activeVersion = note.versionNumber
|
||||||
|
|
||||||
@ -679,7 +685,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteNote(noteID: string): Promise<void | null> {
|
public async deleteNote(noteID: string): Promise<void> {
|
||||||
if (!UUID_REGEX.test(noteID)) {
|
if (!UUID_REGEX.test(noteID)) {
|
||||||
throw new Error("Invalid note ID format.")
|
throw new Error("Invalid note ID format.")
|
||||||
}
|
}
|
||||||
@ -694,7 +700,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
console.log("[NOTE] Deletion successful")
|
console.log("deletion successful")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -707,7 +713,7 @@ class ApiClient {
|
|||||||
public async getNoteHistory(
|
public async getNoteHistory(
|
||||||
noteID: string,
|
noteID: string,
|
||||||
fetchRemote: boolean
|
fetchRemote: boolean
|
||||||
): Promise<VersionMetadata[] | null> {
|
): Promise<VersionMetadata[] | undefined> {
|
||||||
if (!UUID_REGEX.test(noteID)) {
|
if (!UUID_REGEX.test(noteID)) {
|
||||||
throw new Error("Invalid note ID format.")
|
throw new Error("Invalid note ID format.")
|
||||||
}
|
}
|
||||||
@ -715,6 +721,7 @@ class ApiClient {
|
|||||||
if (!fetchRemote) {
|
if (!fetchRemote) {
|
||||||
const cachedVersions = this.loadedHistoryCache.get(noteID)
|
const cachedVersions = this.loadedHistoryCache.get(noteID)
|
||||||
if (cachedVersions != null) {
|
if (cachedVersions != null) {
|
||||||
|
// console.log(`full version cache hit ${noteID}`)
|
||||||
return cachedVersions
|
return cachedVersions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -732,8 +739,8 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
const versions = this.deserializeVersionMetadatas(data)
|
const versions = this.deserializeVersionMetadatas(data)
|
||||||
|
|
||||||
|
console.log(`got ${versions.length} version metadata results, caching ${noteID}`)
|
||||||
this.loadedHistoryCache.set(noteID, versions)
|
this.loadedHistoryCache.set(noteID, versions)
|
||||||
console.log(`[VER] Got and cached ${versions.length} version metadata results`)
|
|
||||||
|
|
||||||
return versions
|
return versions
|
||||||
},
|
},
|
||||||
@ -741,7 +748,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createVersion(noteID: string, title: string, content: string): Promise<void | null> {
|
public async createVersion(noteID: string, title: string, content: string): Promise<void> {
|
||||||
if (!UUID_REGEX.test(noteID)) {
|
if (!UUID_REGEX.test(noteID)) {
|
||||||
throw new Error("Invalid note ID format.")
|
throw new Error("Invalid note ID format.")
|
||||||
}
|
}
|
||||||
@ -760,7 +767,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
console.log("[VER] Creation successful")
|
console.log("creation successful")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -770,7 +777,7 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFullVersion(noteID: string, versionID: string): Promise<FullNote | null> {
|
public async getFullVersion(noteID: string, versionID: string): Promise<FullNote | undefined> {
|
||||||
if (!UUID_REGEX.test(noteID)) {
|
if (!UUID_REGEX.test(noteID)) {
|
||||||
throw new Error("Invalid note ID format.")
|
throw new Error("Invalid note ID format.")
|
||||||
}
|
}
|
||||||
@ -779,13 +786,18 @@ class ApiClient {
|
|||||||
throw new Error("Invalid version ID format.")
|
throw new Error("Invalid version ID format.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: No need to explicitly prevent attempting a cache hit as versions aren't editable
|
// NOTE: Versions aren't editable so we don't need to prevent the system from attempting
|
||||||
|
// to locate each request's contents first from the cache
|
||||||
|
|
||||||
const cachedVersion = this.loadedVersionsCache.get(noteID + versionID)
|
const cachedVersion = this.loadedVersionsCache.get(noteID + versionID)
|
||||||
if (cachedVersion != null) {
|
if (cachedVersion != null) {
|
||||||
|
// console.log(`full version cache hit [${noteID}, ${versionID}]`)
|
||||||
return cachedVersion
|
return cachedVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: check if the requested version is the current version -> use `loadedNotesCache`
|
||||||
|
// (we probably have to modify the caching mechanism so we can look the regular note items up using versionID)
|
||||||
|
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/notes/${noteID}/${versionID}`, {
|
const response = await fetch(`${this.baseUrl}/notes/${noteID}/${versionID}`, {
|
||||||
@ -800,10 +812,10 @@ class ApiClient {
|
|||||||
const version = this.joinDeserializedVersion(noteID, data)
|
const version = this.joinDeserializedVersion(noteID, data)
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[CACHE] Storing [${noteID}, ${versionID}]`)
|
console.log(`caching [${noteID}, ${versionID}]`)
|
||||||
this.loadedVersionsCache.set(noteID + versionID, version)
|
this.loadedVersionsCache.set(noteID + versionID, version)
|
||||||
|
|
||||||
return version
|
return version
|
||||||
|
@ -164,22 +164,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteNote = async (noteID: string) => {
|
|
||||||
try {
|
|
||||||
await apiClient.deleteNote(noteID)
|
|
||||||
|
|
||||||
// If we're deleting the currently active note, clear the current note
|
|
||||||
if ($currentFullNote && $currentFullNote.id === noteID) {
|
|
||||||
currentFullNote.set(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh the notes list due to updates being pushed to server
|
|
||||||
await loadNotes()
|
|
||||||
} catch (err) {
|
|
||||||
cError.set(`Failed to delete note: ${err instanceof Error ? err.message : "Unknown error"}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
await apiClient.logout()
|
await apiClient.logout()
|
||||||
}
|
}
|
||||||
@ -206,7 +190,6 @@
|
|||||||
{logout}
|
{logout}
|
||||||
{createNewNote}
|
{createNewNote}
|
||||||
{selectNote}
|
{selectNote}
|
||||||
{deleteNote}
|
|
||||||
on:close={closeSidebar}
|
on:close={closeSidebar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import type { NoteMetadata, FullNote } from "$lib/client"
|
import type { NoteMetadata, FullNote } from "$lib/client"
|
||||||
import Close from "$lib/icons/Close.svelte"
|
import Close from "$lib/icons/Close.svelte"
|
||||||
import CreateNew from "$lib/icons/CreateNew.svelte"
|
import CreateNew from "$lib/icons/CreateNew.svelte"
|
||||||
import Delete from "$lib/icons/Delete.svelte"
|
|
||||||
import Logout from "$lib/icons/Logout.svelte"
|
import Logout from "$lib/icons/Logout.svelte"
|
||||||
import Search from "$lib/icons/Search.svelte"
|
import Search from "$lib/icons/Search.svelte"
|
||||||
import Settings from "$lib/icons/Settings.svelte"
|
import Settings from "$lib/icons/Settings.svelte"
|
||||||
@ -14,8 +13,7 @@
|
|||||||
export let toggleSettings: () => void
|
export let toggleSettings: () => void
|
||||||
export let logout: () => Promise<void>
|
export let logout: () => Promise<void>
|
||||||
export let createNewNote: () => Promise<void>
|
export let createNewNote: () => Promise<void>
|
||||||
export let selectNote: (noteID: string, fetchRemote: boolean) => Promise<void>
|
export let selectNote: (noteId: string, fetchRemote: boolean) => Promise<void>
|
||||||
export let deleteNote: (noteID: string) => Promise<void>
|
|
||||||
|
|
||||||
const formatDate = (dateString: string | Date): string => {
|
const formatDate = (dateString: string | Date): string => {
|
||||||
if (!dateString) {
|
if (!dateString) {
|
||||||
@ -37,14 +35,6 @@
|
|||||||
sidebarOpen = false
|
sidebarOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteNote = (event: MouseEvent, noteID: string) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
if (confirm("Are you sure you want to delete this note?")) {
|
|
||||||
deleteNote(noteID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNoteKeydown = (event: KeyboardEvent, noteID: string) => {
|
const handleNoteKeydown = (event: KeyboardEvent, noteID: string) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault() // Prevent page scroll on space
|
event.preventDefault() // Prevent page scroll on space
|
||||||
@ -113,24 +103,10 @@
|
|||||||
role="option"
|
role="option"
|
||||||
aria-selected={currentNote && note.id === currentNote.id}
|
aria-selected={currentNote && note.id === currentNote.id}
|
||||||
>
|
>
|
||||||
<div class="sidebar-item-content">
|
|
||||||
<!-- Note metadata description -->
|
|
||||||
<h3 class="truncate font-bold">{note.title || "Untitled Note"}</h3>
|
<h3 class="truncate font-bold">{note.title || "Untitled Note"}</h3>
|
||||||
<div class="sidebar-item-bottom-row">
|
|
||||||
<p class="sidebar-item-text mt-1 italic">
|
<p class="sidebar-item-text mt-1 italic">
|
||||||
{formatDate(note.updatedAt)}
|
{formatDate(note.updatedAt)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Note delete button -->
|
|
||||||
<button
|
|
||||||
on:click={(e) => handleDeleteNote(e, note.id)}
|
|
||||||
class="sidebar-item-delete"
|
|
||||||
aria-label="Delete note"
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// will automatically be proxied to the correct destination
|
// will automatically be proxied to the correct destination
|
||||||
export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
||||||
|
|
||||||
// Lifetimes of *in-memory* authentication tokens in milliseconds
|
// Lifetimes of *in-memory* authentication tokens
|
||||||
export const AT_EXP_MS = 15 * 60 * 1000 // 15 min.
|
export const AT_EXP_MS = 15 * 60 * 1000 // 15 min.
|
||||||
export const CSRF_EXP_MS = 12 * 60 * 60 * 1000 // 12 h.
|
export const CSRF_EXP_MS = 12 * 60 * 60 * 1000 // 12 h.
|
||||||
export const REFRESH_BUF = 30 * 1000 // 30 s.
|
export const REFRESH_BUF = 30 * 1000 // 30 s.
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
|
||||||
<path d="M10 12V17" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
||||||
<path d="M14 12V17" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
||||||
<path d="M4 7H20" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
||||||
<path
|
|
||||||
d="M6 10V18C6 19.6569 7.34315 21 9 21H15C16.6569 21 18 19.6569 18 18V10"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 673 B |
Loading…
x
Reference in New Issue
Block a user