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")
 | 
					    @GetMapping("/auth/access")
 | 
				
			||||||
    public Object getAccessToken(HttpServletRequest request) {
 | 
					    public AccessTokenResponse getAccessToken(HttpServletRequest request) {
 | 
				
			||||||
        return Map.of("accessToken", tokenService.generateAccessToken(request));
 | 
					        return tokenService.generateAccessToken(request);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @DeleteMapping("/auth/refresh-tokens")
 | 
					    @DeleteMapping("/auth/refresh-tokens")
 | 
				
			||||||
    public void removeAllRefreshTokens(@AuthenticationPrincipal User user) {
 | 
					    public void removeAllRefreshTokens(@AuthenticationPrincipal User user) {
 | 
				
			||||||
        tokenService.removeAllRefreshTokens(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;
 | 
					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;
 | 
					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.LoginRequest;
 | 
				
			||||||
import com.andrewlalis.onyx.auth.api.TokenPair;
 | 
					import com.andrewlalis.onyx.auth.api.TokenPair;
 | 
				
			||||||
import com.andrewlalis.onyx.auth.model.RefreshToken;
 | 
					import com.andrewlalis.onyx.auth.model.RefreshToken;
 | 
				
			||||||
| 
						 | 
					@ -14,7 +15,6 @@ import jakarta.servlet.http.HttpServletRequest;
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import lombok.extern.slf4j.Slf4j;
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
import org.springframework.http.HttpStatus;
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 | 
					 | 
				
			||||||
import org.springframework.security.crypto.password.PasswordEncoder;
 | 
					import org.springframework.security.crypto.password.PasswordEncoder;
 | 
				
			||||||
import org.springframework.stereotype.Service;
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
import org.springframework.transaction.annotation.Transactional;
 | 
					import org.springframework.transaction.annotation.Transactional;
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ import java.time.Instant;
 | 
				
			||||||
import java.time.LocalDateTime;
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
import java.time.OffsetDateTime;
 | 
					import java.time.OffsetDateTime;
 | 
				
			||||||
import java.time.ZoneOffset;
 | 
					import java.time.ZoneOffset;
 | 
				
			||||||
 | 
					import java.time.temporal.ChronoUnit;
 | 
				
			||||||
import java.util.Date;
 | 
					import java.util.Date;
 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,6 +39,8 @@ import java.util.Optional;
 | 
				
			||||||
public class TokenService {
 | 
					public class TokenService {
 | 
				
			||||||
    private static final String BEARER_PREFIX = "Bearer ";
 | 
					    private static final String BEARER_PREFIX = "Bearer ";
 | 
				
			||||||
    private static final String ISSUER = "Onyx API";
 | 
					    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 PrivateKey signingKey;
 | 
				
			||||||
    private final PasswordEncoder passwordEncoder;
 | 
					    private final PasswordEncoder passwordEncoder;
 | 
				
			||||||
| 
						 | 
					@ -54,14 +57,16 @@ public class TokenService {
 | 
				
			||||||
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
 | 
					            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        User user = optionalUser.get();
 | 
					        User user = optionalUser.get();
 | 
				
			||||||
        String refreshToken = generateRefreshToken(user);
 | 
					        final Instant now = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
 | 
				
			||||||
        String accessToken = generateAccessToken(refreshToken);
 | 
					        Instant refreshTokenExpiration = now.plus(REFRESH_TOKEN_EXPIRATION_DAYS, ChronoUnit.DAYS);
 | 
				
			||||||
        return new TokenPair(refreshToken, accessToken);
 | 
					        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
 | 
					    @Transactional
 | 
				
			||||||
    public String generateRefreshToken(User user) {
 | 
					    public String generateRefreshToken(User user, Instant expiresAt) {
 | 
				
			||||||
        Instant expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusDays(7).toInstant();
 | 
					 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            String token = Jwts.builder()
 | 
					            String token = Jwts.builder()
 | 
				
			||||||
                    .setSubject(Long.toString(user.getId()))
 | 
					                    .setSubject(Long.toString(user.getId()))
 | 
				
			||||||
| 
						 | 
					@ -86,7 +91,7 @@ public class TokenService {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Transactional(readOnly = true)
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
    public String generateAccessToken(String refreshTokenString) {
 | 
					    public String generateAccessToken(String refreshTokenString, Instant expiresAt) {
 | 
				
			||||||
        String suffix = refreshTokenString.substring(refreshTokenString.length() - RefreshToken.SUFFIX_LENGTH);
 | 
					        String suffix = refreshTokenString.substring(refreshTokenString.length() - RefreshToken.SUFFIX_LENGTH);
 | 
				
			||||||
        RefreshToken refreshToken = null;
 | 
					        RefreshToken refreshToken = null;
 | 
				
			||||||
        for (RefreshToken possibleToken : refreshTokenRepository.findAllByTokenSuffix(suffix)) {
 | 
					        for (RefreshToken possibleToken : refreshTokenRepository.findAllByTokenSuffix(suffix)) {
 | 
				
			||||||
| 
						 | 
					@ -101,7 +106,6 @@ public class TokenService {
 | 
				
			||||||
        if (refreshToken.getExpiresAt().isBefore(LocalDateTime.now(ZoneOffset.UTC))) {
 | 
					        if (refreshToken.getExpiresAt().isBefore(LocalDateTime.now(ZoneOffset.UTC))) {
 | 
				
			||||||
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Refresh token is expired.");
 | 
					            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Refresh token is expired.");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Instant expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1).toInstant();
 | 
					 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            return Jwts.builder()
 | 
					            return Jwts.builder()
 | 
				
			||||||
                    .setSubject(Long.toString(refreshToken.getUser().getId()))
 | 
					                    .setSubject(Long.toString(refreshToken.getUser().getId()))
 | 
				
			||||||
| 
						 | 
					@ -119,12 +123,16 @@ public class TokenService {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Transactional(readOnly = true)
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
    public String generateAccessToken(HttpServletRequest request) {
 | 
					    public AccessTokenResponse generateAccessToken(HttpServletRequest request) {
 | 
				
			||||||
        String refreshTokenString = extractBearerToken(request);
 | 
					        String refreshTokenString = extractBearerToken(request);
 | 
				
			||||||
        if (refreshTokenString == null) {
 | 
					        if (refreshTokenString == null) {
 | 
				
			||||||
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing refresh token.");
 | 
					            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
 | 
					    @Transactional
 | 
				
			||||||
| 
						 | 
					@ -132,6 +140,16 @@ public class TokenService {
 | 
				
			||||||
        refreshTokenRepository.deleteAllByUserId(user.getId());
 | 
					        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 {
 | 
					    public Jws<Claims> getToken(HttpServletRequest request) throws Exception {
 | 
				
			||||||
        String rawToken = extractBearerToken(request);
 | 
					        String rawToken = extractBearerToken(request);
 | 
				
			||||||
        if (rawToken == null) return null;
 | 
					        if (rawToken == null) return null;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,7 @@ public class SecurityConfig {
 | 
				
			||||||
        http.authorizeHttpRequests(registry -> {
 | 
					        http.authorizeHttpRequests(registry -> {
 | 
				
			||||||
            registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/auth/login")).permitAll();
 | 
					            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/access")).permitAll();
 | 
				
			||||||
 | 
					            registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/auth/token-expiration")).permitAll();
 | 
				
			||||||
            registry.anyRequest().authenticated();
 | 
					            registry.anyRequest().authenticated();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        http.csrf(AbstractHttpConfigurer::disable);
 | 
					        http.csrf(AbstractHttpConfigurer::disable);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@
 | 
				
			||||||
      "name": "onyx-web-app",
 | 
					      "name": "onyx-web-app",
 | 
				
			||||||
      "version": "0.0.0",
 | 
					      "version": "0.0.0",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "axios": "^1.6.0",
 | 
				
			||||||
        "pinia": "^2.1.7",
 | 
					        "pinia": "^2.1.7",
 | 
				
			||||||
        "vue": "^3.3.4",
 | 
					        "vue": "^3.3.4",
 | 
				
			||||||
        "vue-router": "^4.2.5"
 | 
					        "vue-router": "^4.2.5"
 | 
				
			||||||
| 
						 | 
					@ -1241,6 +1242,21 @@
 | 
				
			||||||
        "node": ">=8"
 | 
					        "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": {
 | 
					    "node_modules/balanced-match": {
 | 
				
			||||||
      "version": "1.0.2",
 | 
					      "version": "1.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -1318,6 +1334,17 @@
 | 
				
			||||||
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
 | 
					      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
 | 
				
			||||||
      "dev": true
 | 
					      "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": {
 | 
					    "node_modules/computeds": {
 | 
				
			||||||
      "version": "0.0.1",
 | 
					      "version": "0.0.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
 | 
					      "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==",
 | 
					      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
 | 
				
			||||||
      "dev": true
 | 
					      "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": {
 | 
					    "node_modules/dir-glob": {
 | 
				
			||||||
      "version": "3.0.1",
 | 
					      "version": "3.0.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -1755,6 +1790,38 @@
 | 
				
			||||||
      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
 | 
					      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
 | 
				
			||||||
      "dev": true
 | 
					      "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": {
 | 
					    "node_modules/fs.realpath": {
 | 
				
			||||||
      "version": "1.0.0",
 | 
					      "version": "1.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
 | 
				
			||||||
| 
						 | 
					@ -2166,6 +2233,25 @@
 | 
				
			||||||
        "node": ">=8.6"
 | 
					        "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": {
 | 
					    "node_modules/minimatch": {
 | 
				
			||||||
      "version": "3.1.2",
 | 
					      "version": "3.1.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -2564,6 +2650,11 @@
 | 
				
			||||||
        "node": ">= 0.8.0"
 | 
					        "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": {
 | 
					    "node_modules/punycode": {
 | 
				
			||||||
      "version": "2.3.0",
 | 
					      "version": "2.3.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
 | 
					      "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"
 | 
					    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "axios": "^1.6.0",
 | 
				
			||||||
    "pinia": "^2.1.7",
 | 
					    "pinia": "^2.1.7",
 | 
				
			||||||
    "vue": "^3.3.4",
 | 
					    "vue": "^3.3.4",
 | 
				
			||||||
    "vue-router": "^4.2.5"
 | 
					    "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