Created structure for web-app's js client library for consuming onyx api.
This commit is contained in:
parent
321bad46a7
commit
c7bf9d9058
|
@ -0,0 +1,6 @@
|
|||
package com.andrewlalis.onyx.auth.api;
|
||||
|
||||
public record AccessTokenResponse(
|
||||
String accessToken,
|
||||
long expiresAt
|
||||
) {}
|
|
@ -20,12 +20,17 @@ public class AuthController {
|
|||
}
|
||||
|
||||
@GetMapping("/auth/access")
|
||||
public Object getAccessToken(HttpServletRequest request) {
|
||||
return Map.of("accessToken", tokenService.generateAccessToken(request));
|
||||
public AccessTokenResponse getAccessToken(HttpServletRequest request) {
|
||||
return tokenService.generateAccessToken(request);
|
||||
}
|
||||
|
||||
@DeleteMapping("/auth/refresh-tokens")
|
||||
public void removeAllRefreshTokens(@AuthenticationPrincipal User user) {
|
||||
tokenService.removeAllRefreshTokens(user);
|
||||
}
|
||||
|
||||
@GetMapping("/auth/token-expiration")
|
||||
public Object getTokenExpiration(HttpServletRequest request) {
|
||||
return Map.of("expiresAt", tokenService.getTokenExpiration(request));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
package com.andrewlalis.onyx.auth.api;
|
||||
|
||||
public record TokenPair(String refreshToken, String accessToken) {}
|
||||
public record TokenPair(
|
||||
String refreshToken,
|
||||
long refreshTokenExpiresAt,
|
||||
String accessToken,
|
||||
long accessTokenExpiresAt
|
||||
) {}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.andrewlalis.onyx.auth.service;
|
||||
|
||||
import com.andrewlalis.onyx.auth.api.AccessTokenResponse;
|
||||
import com.andrewlalis.onyx.auth.api.LoginRequest;
|
||||
import com.andrewlalis.onyx.auth.api.TokenPair;
|
||||
import com.andrewlalis.onyx.auth.model.RefreshToken;
|
||||
|
@ -14,7 +15,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
@ -29,6 +29,7 @@ import java.time.Instant;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
|
@ -38,6 +39,8 @@ import java.util.Optional;
|
|||
public class TokenService {
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
private static final String ISSUER = "Onyx API";
|
||||
private static final int REFRESH_TOKEN_EXPIRATION_DAYS = 30;
|
||||
private static final int ACCESS_TOKEN_EXPIRATION_MINUTES = 120;
|
||||
|
||||
private PrivateKey signingKey;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
@ -54,14 +57,16 @@ public class TokenService {
|
|||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
|
||||
}
|
||||
User user = optionalUser.get();
|
||||
String refreshToken = generateRefreshToken(user);
|
||||
String accessToken = generateAccessToken(refreshToken);
|
||||
return new TokenPair(refreshToken, accessToken);
|
||||
final Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
|
||||
Instant refreshTokenExpiration = now.plus(REFRESH_TOKEN_EXPIRATION_DAYS, ChronoUnit.DAYS);
|
||||
String refreshToken = generateRefreshToken(user, refreshTokenExpiration);
|
||||
Instant accessTokenExpiration = now.plus(ACCESS_TOKEN_EXPIRATION_MINUTES, ChronoUnit.MINUTES);
|
||||
String accessToken = generateAccessToken(refreshToken, accessTokenExpiration);
|
||||
return new TokenPair(refreshToken, refreshTokenExpiration.toEpochMilli(), accessToken, accessTokenExpiration.toEpochMilli());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public String generateRefreshToken(User user) {
|
||||
Instant expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusDays(7).toInstant();
|
||||
public String generateRefreshToken(User user, Instant expiresAt) {
|
||||
try {
|
||||
String token = Jwts.builder()
|
||||
.setSubject(Long.toString(user.getId()))
|
||||
|
@ -86,7 +91,7 @@ public class TokenService {
|
|||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public String generateAccessToken(String refreshTokenString) {
|
||||
public String generateAccessToken(String refreshTokenString, Instant expiresAt) {
|
||||
String suffix = refreshTokenString.substring(refreshTokenString.length() - RefreshToken.SUFFIX_LENGTH);
|
||||
RefreshToken refreshToken = null;
|
||||
for (RefreshToken possibleToken : refreshTokenRepository.findAllByTokenSuffix(suffix)) {
|
||||
|
@ -101,7 +106,6 @@ public class TokenService {
|
|||
if (refreshToken.getExpiresAt().isBefore(LocalDateTime.now(ZoneOffset.UTC))) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Refresh token is expired.");
|
||||
}
|
||||
Instant expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1).toInstant();
|
||||
try {
|
||||
return Jwts.builder()
|
||||
.setSubject(Long.toString(refreshToken.getUser().getId()))
|
||||
|
@ -119,12 +123,16 @@ public class TokenService {
|
|||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public String generateAccessToken(HttpServletRequest request) {
|
||||
public AccessTokenResponse generateAccessToken(HttpServletRequest request) {
|
||||
String refreshTokenString = extractBearerToken(request);
|
||||
if (refreshTokenString == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing refresh token.");
|
||||
}
|
||||
return generateAccessToken(refreshTokenString);
|
||||
Instant expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusMinutes(ACCESS_TOKEN_EXPIRATION_MINUTES).toInstant();
|
||||
return new AccessTokenResponse(
|
||||
generateAccessToken(refreshTokenString, expiresAt),
|
||||
expiresAt.toEpochMilli()
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
@ -132,6 +140,16 @@ public class TokenService {
|
|||
refreshTokenRepository.deleteAllByUserId(user.getId());
|
||||
}
|
||||
|
||||
public long getTokenExpiration(HttpServletRequest request) {
|
||||
try {
|
||||
Jws<Claims> jws = getToken(request);
|
||||
return jws.getBody().getExpiration().getTime();
|
||||
} catch (Exception e) {
|
||||
log.warn("Exception occurred while getting token expiration.", e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public Jws<Claims> getToken(HttpServletRequest request) throws Exception {
|
||||
String rawToken = extractBearerToken(request);
|
||||
if (rawToken == null) return null;
|
||||
|
|
|
@ -27,6 +27,7 @@ public class SecurityConfig {
|
|||
http.authorizeHttpRequests(registry -> {
|
||||
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/auth/login")).permitAll();
|
||||
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/auth/access")).permitAll();
|
||||
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/auth/token-expiration")).permitAll();
|
||||
registry.anyRequest().authenticated();
|
||||
});
|
||||
http.csrf(AbstractHttpConfigurer::disable);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"name": "onyx-web-app",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.5"
|
||||
|
@ -1241,6 +1242,21 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
|
||||
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
@ -1318,6 +1334,17 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/computeds": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
|
||||
|
@ -1390,6 +1417,14 @@
|
|||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
|
@ -1755,6 +1790,38 @@
|
|||
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
|
||||
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
|
@ -2166,6 +2233,25 @@
|
|||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
|
@ -2564,6 +2650,11 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.5"
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import type {OnyxApi} from "@/api-client/onyx-api";
|
||||
|
||||
export interface LoginInfo {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface TokenData {
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
interface TokenPair {
|
||||
accessToken: TokenData;
|
||||
refreshToken: TokenData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication endpoints in the Onyx API.
|
||||
*/
|
||||
export class OnyxAuthApi {
|
||||
constructor(readonly api: OnyxApi) {}
|
||||
|
||||
async login(loginInfo: LoginInfo): Promise<TokenPair> {
|
||||
const data: any = await this.api.request({method: "POST", url: "/auth/login", data: loginInfo}, true)
|
||||
return {
|
||||
accessToken: {token: data.accessToken, expiresAt: data.accessTokenExpiresAt},
|
||||
refreshToken: {token: data.refreshToken, expiresAt: data.refreshTokenExpiresAt}
|
||||
}
|
||||
}
|
||||
|
||||
async getAccessToken(refreshToken: string): Promise<TokenData> {
|
||||
const data: any = await this.api.request({url: "/auth/access", headers: {"Authorization": "Bearer " + refreshToken}});
|
||||
return {
|
||||
token: data.accessToken,
|
||||
expiresAt: data.expiresAt
|
||||
};
|
||||
}
|
||||
|
||||
async getTokenExpiration(token: string): Promise<number> {
|
||||
const data: any = await this.api.request({url: "/auth/token-expiration", headers: {"Authorization": "Bearer " + token}}, true);
|
||||
return data.expiresAt;
|
||||
}
|
||||
|
||||
async removeAllRefreshTokens(): Promise<void> {
|
||||
await this.api.request({method: "DELETE", url: "/auth/refresh-tokens"})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import type {OnyxApi} from "@/api-client/onyx-api";
|
||||
|
||||
export interface ContentNode {
|
||||
id: number;
|
||||
name: string;
|
||||
nodeType: string;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface ContainerChildData {
|
||||
id: number;
|
||||
name: string;
|
||||
nodeType: string;
|
||||
}
|
||||
|
||||
export interface ContainerNode extends ContentNode {
|
||||
children: ContainerChildData[]
|
||||
}
|
||||
|
||||
export interface DocumentNode extends ContentNode {
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export interface ContainerNodeCreationData {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DocumentNodeCreationData {
|
||||
name: string;
|
||||
contentType: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The API for interacting with nodes in an Onyx instance's content tree.
|
||||
*/
|
||||
export class OnyxContentApi {
|
||||
constructor(readonly api: OnyxApi) {}
|
||||
|
||||
async getNode(id: number): Promise<ContainerNode | DocumentNode> {
|
||||
return await this.api.request({url: "/content/nodes/" + id});
|
||||
}
|
||||
|
||||
async getRoot(): Promise<ContainerNode> {
|
||||
return await this.api.request({url: "/content/nodes/root"});
|
||||
}
|
||||
|
||||
async createContainer(parentNodeId: number, data: ContainerNodeCreationData): Promise<ContainerNode> {
|
||||
return await this.api.request({
|
||||
method: "POST",
|
||||
url: "/content/nodes/" + parentNodeId + "/children",
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
async createDocument(parentNodeId: number, data: DocumentNodeCreationData): Promise<DocumentNode> {
|
||||
return await this.api.request({
|
||||
method: "POST",
|
||||
url: "/content/nodes/" + parentNodeId + "/children",
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
async deleteNode(id: number): Promise<void> {
|
||||
await this.api.request({method: "DELETE", url: "/content/nodes/" + id});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import type {AxiosRequestConfig} from "axios";
|
||||
import axios, {Axios} from "axios";
|
||||
import {OnyxAuthApi} from "@/api-client/onyx-api-auth";
|
||||
import type{LoginInfo, TokenData} from "@/api-client/onyx-api-auth";
|
||||
import {OnyxContentApi} from "@/api-client/onyx-api-content";
|
||||
|
||||
interface OnyxAuthState {
|
||||
accessToken: TokenData | null;
|
||||
refreshToken: TokenData | null;
|
||||
}
|
||||
|
||||
export class OnyxApi {
|
||||
private authState: OnyxAuthState = {accessToken: null, refreshToken: null};
|
||||
private accessTokenIntervalId: number | null = null;
|
||||
private httpClient: Axios;
|
||||
|
||||
readonly auth: OnyxAuthApi = new OnyxAuthApi(this);
|
||||
readonly content: OnyxContentApi = new OnyxContentApi(this);
|
||||
|
||||
constructor() {
|
||||
this.httpClient = axios.create({
|
||||
baseURL: "http://localhost:8080/",
|
||||
timeout: 3000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a new API instance from information in the user's local storage,
|
||||
* which should ideally have a stored refresh token and its expiration date.
|
||||
* Using this information, we can fetch a new access token and any other auth
|
||||
* data. Will return an API instance that's authenticated if we have the info
|
||||
* to do so, and all necessary services are available. Otherwise, an
|
||||
* unauthenticated instance is returned.
|
||||
*/
|
||||
static async loadFromLocalStorage(): Promise<OnyxApi> {
|
||||
const refreshToken = localStorage.getItem("onyx-api-refresh-token");
|
||||
const expirationStr = localStorage.getItem("onyx-api-refresh-token-expiration");
|
||||
let expiration = (expirationStr !== null) ? parseInt(expirationStr, 10): null;
|
||||
const api = new OnyxApi();
|
||||
// If no expiration was stored, try and get that first.
|
||||
if (refreshToken && expiration === null) {
|
||||
try {
|
||||
expiration = await api.auth.getTokenExpiration(refreshToken);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return api;
|
||||
}
|
||||
}
|
||||
if (expiration === null) return api; // This is to make the type-checker happy.
|
||||
// If we have a valid refresh token that's not expired, get an access token and init the auth state.
|
||||
if (refreshToken && expiration > Date.now()) {
|
||||
try {
|
||||
api.authState.accessToken = await api.auth.getAccessToken(refreshToken);
|
||||
api.authState.refreshToken = {token: refreshToken, expiresAt: expiration};
|
||||
api.saveAuthStateToLocalStorage();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
private saveAuthStateToLocalStorage() {
|
||||
if (this.authState.refreshToken) {
|
||||
localStorage.setItem("onyx-api-refresh-token", this.authState.refreshToken?.token);
|
||||
localStorage.setItem("onyx-api-refresh-token-expiration", this.authState.refreshToken.expiresAt.toString(10));
|
||||
} else {
|
||||
localStorage.removeItem("onyx-api-refresh-token");
|
||||
localStorage.removeItem("onyx-api-refresh-token-expiration");
|
||||
}
|
||||
console.log("Saved current auth state to local storage.", this.authState.refreshToken);
|
||||
}
|
||||
|
||||
private initAccessTokenRefreshing() {
|
||||
if (this.accessTokenIntervalId) {
|
||||
window.clearInterval(this.accessTokenIntervalId);
|
||||
}
|
||||
this.accessTokenIntervalId = window.setInterval(async () => {
|
||||
if (this.authState.refreshToken && this.authState.accessToken && this.authState.accessToken.expiresAt < Date.now() + 5000) {
|
||||
this.authState.accessToken = await this.auth.getAccessToken(this.authState.refreshToken.token);
|
||||
this.saveAuthStateToLocalStorage();
|
||||
console.log("Updated access token.");
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to do a login with the given credentials to obtain a new refresh token.
|
||||
* @param credentials The credentials to use.
|
||||
*/
|
||||
async login(credentials: LoginInfo): Promise<void> {
|
||||
const tokens = await this.auth.login(credentials);
|
||||
this.authState = {
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken
|
||||
};
|
||||
this.saveAuthStateToLocalStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs this instance out if it was logged in.
|
||||
*/
|
||||
logout() {
|
||||
if (this.accessTokenIntervalId) {
|
||||
window.clearInterval(this.accessTokenIntervalId);
|
||||
}
|
||||
this.authState = {accessToken: null, refreshToken: null};
|
||||
this.saveAuthStateToLocalStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to the Onyx API, setting the Authorization header when
|
||||
* this API instance is authenticated.
|
||||
* @param config The axios request object.
|
||||
* @param anon Whether to make the request anonymous (no auth).
|
||||
*/
|
||||
async request<R>(config: AxiosRequestConfig, anon: boolean = false): Promise<R> {
|
||||
if (!config.headers) config.headers = {};
|
||||
if (!anon && this.isAuthenticated()) {
|
||||
config.headers["Authorization"] = "Bearer " + this.authState.accessToken?.token;
|
||||
}
|
||||
if (config.data) {
|
||||
config.headers["Content-Type"] = "application/json";
|
||||
}
|
||||
const response = await this.httpClient.request(config);
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
}
|
||||
throw response.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether this API instance is authenticated and likely able to
|
||||
* access endpoints that require authentication.
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.authState.accessToken !== null &&
|
||||
this.authState.accessToken.expiresAt > Date.now() &&
|
||||
this.authState.refreshToken !== null &&
|
||||
this.authState.refreshToken.expiresAt > Date.now();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue