style: undefined -> null return types & log source prefix
This commit is contained in:
parent
8e8f5b8faf
commit
337157327c
@ -107,7 +107,7 @@ export const cError: Writable<string | null> = writable(null)
|
||||
class ApiClient {
|
||||
private viewCookieName: string
|
||||
private baseUrl: string
|
||||
private lastAtUpdate = new Date(0)
|
||||
private lastAtUpdate = new Date(0) // Refreshing the page wipes access and CSRF tokens from memory -> Rotation needed
|
||||
private lastCsrfUpdate = new Date(0)
|
||||
private refreshInProgress = false
|
||||
private activeVersion = -1
|
||||
@ -123,7 +123,7 @@ class ApiClient {
|
||||
private async handleRequest<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: { useBearerAuth: boolean }
|
||||
): Promise<T | undefined> {
|
||||
): Promise<T | null> {
|
||||
isPending.set(true)
|
||||
cError.set(null)
|
||||
|
||||
@ -137,18 +137,21 @@ class ApiClient {
|
||||
try {
|
||||
await this.checkAndRefreshAccessToken()
|
||||
} catch (err) {
|
||||
console.log("refresh attempt not successful")
|
||||
console.log("[REQ] Refresh attempt not successful")
|
||||
await this.handleLocalLogout()
|
||||
throw new Error("Session expired, please authenticate again.")
|
||||
}
|
||||
}
|
||||
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
cError.set(err instanceof Error ? err.message : "Unknown error")
|
||||
console.log(`error: ${get(cError)}`)
|
||||
console.log(`[ERR] ${get(cError)}`)
|
||||
} finally {
|
||||
isPending.set(false)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Should be attached to routes that handle authentication with the bearer token (access token)
|
||||
@ -161,12 +164,13 @@ class ApiClient {
|
||||
// This should never happen due to the token expiration checks we make client-side,
|
||||
// but it's still good to have as a fallback
|
||||
try {
|
||||
console.log("unexpected 401 caught, attempting refresh")
|
||||
console.log("[RES] Unexpected 401 caught, attempting to refresh...")
|
||||
await this.checkAndRefreshAccessToken()
|
||||
} catch (err) {
|
||||
console.log("refresh attempt not successful")
|
||||
const errMsg = err instanceof Error ? err.message : "Unknown error"
|
||||
console.log(`[ERR] ${errMsg}`)
|
||||
await this.handleLocalLogout()
|
||||
throw new Error("Session expired, please authenticate again.")
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,20 +196,16 @@ class ApiClient {
|
||||
const timeSinceUpdate = Date.now() - this.lastAtUpdate.getTime()
|
||||
const needsRefresh = timeSinceUpdate > AT_EXP_MS - REFRESH_BUF
|
||||
|
||||
console.log(`timeSinceUpdate: ${timeSinceUpdate}`)
|
||||
|
||||
if (needsRefresh) {
|
||||
console.log("running token refresh attempt")
|
||||
console.log(`[AUTH] Running token refresh attempt (timeSinceUpdate=${timeSinceUpdate})`)
|
||||
|
||||
this.refreshInProgress = true
|
||||
await this.refreshAccessToken()
|
||||
this.refreshInProgress = false
|
||||
} else {
|
||||
console.log("no need to rotate tokens")
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshAccessToken(): Promise<void> {
|
||||
private async refreshAccessToken(): Promise<void | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const response = await fetch(`${this.baseUrl}/auth/cookie/refresh`, {
|
||||
@ -235,7 +235,7 @@ class ApiClient {
|
||||
const needsRefresh = timeSinceUpdate > CSRF_EXP_MS - REFRESH_BUF
|
||||
|
||||
if (!token || needsRefresh) {
|
||||
console.log("refreshing csrf token")
|
||||
console.log("[AUTH] Refreshing CSRF token")
|
||||
await this.refreshCsrfToken()
|
||||
token = get(csrfToken)
|
||||
}
|
||||
@ -243,7 +243,7 @@ class ApiClient {
|
||||
return { "X-Csrf-Token": token || "" }
|
||||
}
|
||||
|
||||
private async refreshCsrfToken(): Promise<void> {
|
||||
private async refreshCsrfToken(): Promise<void | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const response = await fetch(`${this.baseUrl}/auth/cookie/csrf`, {
|
||||
@ -263,7 +263,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
private async handleLocalLogout(): Promise<void> {
|
||||
private async handleLocalLogout(): Promise<void | null> {
|
||||
this.lastAtUpdate = new Date(0)
|
||||
this.lastCsrfUpdate = new Date(0)
|
||||
|
||||
@ -286,7 +286,7 @@ class ApiClient {
|
||||
const viewCookie = this.getCookieValue(this.viewCookieName)
|
||||
|
||||
if (!viewCookie) {
|
||||
console.log("view cookie not found")
|
||||
console.log("[AUTH] View cookie not found")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -294,14 +294,11 @@ class ApiClient {
|
||||
const expirationTimestamp = parseInt(value, 10)
|
||||
|
||||
if (isNaN(expirationTimestamp)) {
|
||||
console.log(`invalid expiration timestamp: ${value}`)
|
||||
console.log(`[ERR] Invalid view cookie expiration timestamp: ${value}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const expirationDate = new Date(expirationTimestamp * 1000)
|
||||
console.log(`auth cookie expiration: ${expirationDate}`)
|
||||
|
||||
return now < expirationDate
|
||||
return now < new Date(expirationTimestamp * 1000)
|
||||
}
|
||||
|
||||
private deleteViewCookie() {
|
||||
@ -378,12 +375,12 @@ class ApiClient {
|
||||
private joinDeserializedVersion(
|
||||
noteID: string,
|
||||
apiResponse: ApiFullVersionResponse
|
||||
): FullNote | undefined {
|
||||
): FullNote | null {
|
||||
// Cache lookups are safe here due to this always being called *after* fetching the actual `FullNote`
|
||||
const cachedNote = this.loadedNotesCache.get(noteID)
|
||||
|
||||
if (!cachedNote) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
@ -399,7 +396,7 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async register(username: string, password: string): Promise<void> {
|
||||
public async register(username: string, password: string): Promise<void | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const response = await fetch(`${this.baseUrl}/auth/signup`, {
|
||||
@ -408,20 +405,21 @@ class ApiClient {
|
||||
body: JSON.stringify({ username, password })
|
||||
})
|
||||
|
||||
// Can't overwrite `username` parameter
|
||||
// Can't overwrite the function parameter
|
||||
const data = await this.handleResponse<{
|
||||
id: string
|
||||
username: string
|
||||
}>(response, { useBearerAuth: false })
|
||||
|
||||
console.log(`${data.username} -> ${data.id}`)
|
||||
console.log(`[USER] Registration of user '${data.username}' successful`)
|
||||
|
||||
await goto("/login")
|
||||
},
|
||||
{ useBearerAuth: false }
|
||||
)
|
||||
}
|
||||
|
||||
public async login(username: string, password: string): Promise<void> {
|
||||
public async login(username: string, password: string): Promise<void | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const response = await fetch(`${this.baseUrl}/auth/login`, {
|
||||
@ -444,7 +442,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async logout(): Promise<void> {
|
||||
public async logout(): Promise<void | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const response = await fetch(`${this.baseUrl}/auth/logout`, {
|
||||
@ -456,7 +454,7 @@ class ApiClient {
|
||||
})
|
||||
|
||||
if (response.status === 204) {
|
||||
console.log("logout successful")
|
||||
console.log("[USER] Logout successful")
|
||||
await this.handleLocalLogout()
|
||||
return
|
||||
}
|
||||
@ -468,7 +466,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async getCurrentUser(): Promise<void> {
|
||||
public async getCurrentUser(): Promise<void | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const response = await fetch(`${this.baseUrl}/auth/me`, {
|
||||
@ -479,15 +477,16 @@ class ApiClient {
|
||||
const data = await this.handleResponse<ApiUserResponse>(response, { useBearerAuth: false })
|
||||
const user = this.deserializeUser(data)
|
||||
|
||||
console.log(user)
|
||||
|
||||
currentUser.set(user)
|
||||
},
|
||||
{ useBearerAuth: true }
|
||||
)
|
||||
}
|
||||
|
||||
public async updateCurrentUserPassword(oldPassword: string, newPassword: string): Promise<void> {
|
||||
public async updateCurrentUserPassword(
|
||||
oldPassword: string,
|
||||
newPassword: string
|
||||
): Promise<void | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const data = {
|
||||
@ -502,21 +501,21 @@ class ApiClient {
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
const { accessToken: token, user } = await this.handleResponse<{
|
||||
accessToken: string
|
||||
user: User
|
||||
}>(response, { useBearerAuth: false })
|
||||
|
||||
accessToken.set(token)
|
||||
currentUser.set(user || null)
|
||||
this.lastAtUpdate = new Date()
|
||||
|
||||
console.log(user)
|
||||
},
|
||||
{ useBearerAuth: true }
|
||||
)
|
||||
}
|
||||
|
||||
public async deleteCurrentUser(password: string): Promise<void> {
|
||||
public async deleteCurrentUser(password: string): Promise<void | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const response = await fetch(`${this.baseUrl}/auth/owner`, {
|
||||
@ -529,7 +528,7 @@ class ApiClient {
|
||||
})
|
||||
|
||||
if (response.status === 204) {
|
||||
console.log("deletion successful")
|
||||
console.log("[USER] Deletion successful")
|
||||
await this.handleLocalLogout()
|
||||
return
|
||||
}
|
||||
@ -541,7 +540,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async adminListAll(): Promise<User[] | undefined> {
|
||||
public async adminListAll(): Promise<User[] | null> {
|
||||
const user = get(currentUser)
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Error("Admin privileges required.")
|
||||
@ -559,7 +558,7 @@ class ApiClient {
|
||||
})
|
||||
|
||||
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
|
||||
},
|
||||
@ -567,7 +566,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async adminDeleteUser(userID: string): Promise<void> {
|
||||
public async adminDeleteUser(userID: string): Promise<void | null> {
|
||||
const user = get(currentUser)
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Error("Admin privileges required.")
|
||||
@ -587,7 +586,7 @@ class ApiClient {
|
||||
})
|
||||
|
||||
if (response.status === 204) {
|
||||
console.log("admin: deletion successful")
|
||||
console.log(`[ADMIN] Deletion of user '${userID}' successful`)
|
||||
return
|
||||
}
|
||||
|
||||
@ -597,7 +596,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async listNotes(): Promise<NoteMetadata[] | undefined> {
|
||||
public async listNotes(): Promise<NoteMetadata[] | null> {
|
||||
return this.handleRequest(
|
||||
async () => {
|
||||
const params = new URLSearchParams()
|
||||
@ -618,7 +617,7 @@ class ApiClient {
|
||||
notes = this.deserializeNoteMetadatas(data)
|
||||
}
|
||||
|
||||
console.log(`got ${notes.length} note metadata results`)
|
||||
console.log(`[NOTE] Got ${notes.length} note metadata results`)
|
||||
|
||||
return notes
|
||||
},
|
||||
@ -626,7 +625,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async createNote(): Promise<NewNoteResponse | undefined> {
|
||||
public async createNote(): Promise<NewNoteResponse | null> {
|
||||
// NOTE: The initial note version doesn't allow any user input, the first user-made modification
|
||||
// is applied through the version creation endpoint
|
||||
|
||||
@ -644,10 +643,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async getActiveFullNote(
|
||||
noteID: string,
|
||||
fetchRemote: boolean
|
||||
): Promise<FullNote | undefined> {
|
||||
public async getActiveFullNote(noteID: string, fetchRemote: boolean): Promise<FullNote | null> {
|
||||
if (!UUID_REGEX.test(noteID)) {
|
||||
throw new Error("Invalid note ID format.")
|
||||
}
|
||||
@ -656,7 +652,6 @@ class ApiClient {
|
||||
if (!fetchRemote) {
|
||||
const cachedNote = this.loadedNotesCache.get(noteID)
|
||||
if (cachedNote != null) {
|
||||
// console.log(`full note cache hit ${noteID}`)
|
||||
this.activeVersion = cachedNote.versionNumber
|
||||
return cachedNote
|
||||
}
|
||||
@ -675,7 +670,7 @@ class ApiClient {
|
||||
})
|
||||
const note = this.deserializeFullNote(data)
|
||||
|
||||
console.log(`caching ${noteID}`)
|
||||
console.log(`[CACHE] Storing ${noteID}`)
|
||||
this.loadedNotesCache.set(noteID, note)
|
||||
this.activeVersion = note.versionNumber
|
||||
|
||||
@ -685,7 +680,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async deleteNote(noteID: string): Promise<void> {
|
||||
public async deleteNote(noteID: string): Promise<void | null> {
|
||||
if (!UUID_REGEX.test(noteID)) {
|
||||
throw new Error("Invalid note ID format.")
|
||||
}
|
||||
@ -700,7 +695,7 @@ class ApiClient {
|
||||
})
|
||||
|
||||
if (response.status === 204) {
|
||||
console.log("deletion successful")
|
||||
console.log("[NOTE] Deletion successful")
|
||||
return
|
||||
}
|
||||
|
||||
@ -713,7 +708,7 @@ class ApiClient {
|
||||
public async getNoteHistory(
|
||||
noteID: string,
|
||||
fetchRemote: boolean
|
||||
): Promise<VersionMetadata[] | undefined> {
|
||||
): Promise<VersionMetadata[] | null> {
|
||||
if (!UUID_REGEX.test(noteID)) {
|
||||
throw new Error("Invalid note ID format.")
|
||||
}
|
||||
@ -721,7 +716,6 @@ class ApiClient {
|
||||
if (!fetchRemote) {
|
||||
const cachedVersions = this.loadedHistoryCache.get(noteID)
|
||||
if (cachedVersions != null) {
|
||||
// console.log(`full version cache hit ${noteID}`)
|
||||
return cachedVersions
|
||||
}
|
||||
}
|
||||
@ -739,8 +733,8 @@ class ApiClient {
|
||||
})
|
||||
const versions = this.deserializeVersionMetadatas(data)
|
||||
|
||||
console.log(`got ${versions.length} version metadata results, caching ${noteID}`)
|
||||
this.loadedHistoryCache.set(noteID, versions)
|
||||
console.log(`[VER] Got and cached ${versions.length} version metadata results`)
|
||||
|
||||
return versions
|
||||
},
|
||||
@ -748,7 +742,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async createVersion(noteID: string, title: string, content: string): Promise<void> {
|
||||
public async createVersion(noteID: string, title: string, content: string): Promise<void | null> {
|
||||
if (!UUID_REGEX.test(noteID)) {
|
||||
throw new Error("Invalid note ID format.")
|
||||
}
|
||||
@ -767,7 +761,7 @@ class ApiClient {
|
||||
})
|
||||
|
||||
if (response.status === 204) {
|
||||
console.log("creation successful")
|
||||
console.log("[VER] Creation successful")
|
||||
return
|
||||
}
|
||||
|
||||
@ -777,7 +771,7 @@ class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
public async getFullVersion(noteID: string, versionID: string): Promise<FullNote | undefined> {
|
||||
public async getFullVersion(noteID: string, versionID: string): Promise<FullNote | null> {
|
||||
if (!UUID_REGEX.test(noteID)) {
|
||||
throw new Error("Invalid note ID format.")
|
||||
}
|
||||
@ -786,18 +780,13 @@ class ApiClient {
|
||||
throw new Error("Invalid version ID format.")
|
||||
}
|
||||
|
||||
// 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
|
||||
// NOTE: No need to explicitly prevent attempting a cache hit as versions aren't editable
|
||||
|
||||
const cachedVersion = this.loadedVersionsCache.get(noteID + versionID)
|
||||
if (cachedVersion != null) {
|
||||
// console.log(`full version cache hit [${noteID}, ${versionID}]`)
|
||||
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(
|
||||
async () => {
|
||||
const response = await fetch(`${this.baseUrl}/notes/${noteID}/${versionID}`, {
|
||||
@ -812,10 +801,10 @@ class ApiClient {
|
||||
const version = this.joinDeserializedVersion(noteID, data)
|
||||
|
||||
if (!version) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
console.log(`caching [${noteID}, ${versionID}]`)
|
||||
console.log(`[CACHE] Storing [${noteID}, ${versionID}]`)
|
||||
this.loadedVersionsCache.set(noteID + versionID, version)
|
||||
|
||||
return version
|
||||
|
@ -2,7 +2,7 @@
|
||||
// will automatically be proxied to the correct destination
|
||||
export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
||||
|
||||
// Lifetimes of *in-memory* authentication tokens
|
||||
// Lifetimes of *in-memory* authentication tokens in milliseconds
|
||||
export const AT_EXP_MS = 15 * 60 * 1000 // 15 min.
|
||||
export const CSRF_EXP_MS = 12 * 60 * 60 * 1000 // 12 h.
|
||||
export const REFRESH_BUF = 30 * 1000 // 30 s.
|
||||
|
Loading…
x
Reference in New Issue
Block a user