diff --git a/app.json b/app.json
index a20dee2..30a2a8d 100644
--- a/app.json
+++ b/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "모아동",
"slug": "moadong-app",
- "version": "1.7.0",
+ "version": "1.7.1",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "moadongapp",
@@ -10,7 +10,7 @@
"newArchEnabled": true,
"ios": {
"supportsTablet": false,
- "buildNumber": "17",
+ "buildNumber": "18",
"googleServicesFile": "./GoogleService-Info.plist",
"bundleIdentifier": "com.moadong.moadong",
"associatedDomains": [
@@ -26,7 +26,7 @@
},
"android": {
"jsEngine": "hermes",
- "versionCode": 17,
+ "versionCode": 18,
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
diff --git a/ios/app.xcodeproj/project.pbxproj b/ios/app.xcodeproj/project.pbxproj
index 82e08c2..bb22420 100644
--- a/ios/app.xcodeproj/project.pbxproj
+++ b/ios/app.xcodeproj/project.pbxproj
@@ -417,7 +417,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = app/app.entitlements;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 2QMK9GBWN6;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -430,7 +430,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.7.0;
+ MARKETING_VERSION = 1.7.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -454,7 +454,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = app/app.entitlements;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 2QMK9GBWN6;
INFOPLIST_FILE = app/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
@@ -462,7 +462,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.7.0;
+ MARKETING_VERSION = 1.7.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
diff --git a/ios/app/Info.plist b/ios/app/Info.plist
index bde57a7..dfb41e5 100644
--- a/ios/app/Info.plist
+++ b/ios/app/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.7.0
+ 1.7.1
CFBundleSignature
????
CFBundleURLTypes
@@ -39,7 +39,7 @@
CFBundleVersion
- 17
+ 18
LSMinimumSystemVersion
12.0
LSRequiresIPhoneOS
diff --git a/services/api.ts b/services/api.ts
index 5a886e2..b470690 100644
--- a/services/api.ts
+++ b/services/api.ts
@@ -6,7 +6,7 @@ import { ApiErrorResponse } from '@/types/club.types';
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import * as Application from 'expo-application';
import Constants from 'expo-constants';
-import { getOrCreateAuthSubject, getStoredAccessToken, saveAccessToken } from './auth-token-storage';
+import { getStoredAccessToken, resolveAuthSubject, saveAccessToken } from './auth-token-storage';
const APP_VERSION_HEADER_KEY = 'APP_VERSION';
@@ -160,7 +160,7 @@ function attachInterceptors(client: AxiosInstance, options: { withAuth: boolean
if (!refreshTokenPromise) {
refreshTokenPromise = (async () => {
try {
- const sub = await getOrCreateAuthSubject();
+ const sub = await resolveAuthSubject();
const iat = Math.floor(Date.now() / 1000);
const response = await publicApiClient.post('/auth/student', { sub, iat });
const nextToken = extractAccessToken(response.data);
diff --git a/services/auth-token-storage.ts b/services/auth-token-storage.ts
index 0d9220a..b01d929 100644
--- a/services/auth-token-storage.ts
+++ b/services/auth-token-storage.ts
@@ -6,13 +6,20 @@ const AUTH_SUBJECT_KEY = '@auth_subject';
let cachedAccessToken: string | null | undefined;
let cachedAuthSubject: string | null | undefined;
+// sub는 서버가 발급하는 신원(studentId)의 근거가 되므로 추측 가능한 값이면 안 된다.
+// crypto.getRandomValues는 app/_layout.tsx의 react-native-get-random-values로 폴리필된다.
function generateUuidV4(): string {
- const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
- return template.replace(/[xy]/g, (char) => {
- const random = Math.floor(Math.random() * 16);
- const value = char === 'x' ? random : (random & 0x3) | 0x8;
- return value.toString(16);
- });
+ if (typeof globalThis.crypto?.getRandomValues !== 'function') {
+ throw new Error('crypto.getRandomValues를 사용할 수 없어 auth subject를 생성할 수 없습니다.');
+ }
+
+ const bytes = new Uint8Array(16);
+ globalThis.crypto.getRandomValues(bytes);
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10
+
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
export async function getStoredAccessToken(): Promise {
@@ -87,3 +94,18 @@ export function getJwtSubject(accessToken: string): string | null {
return null;
}
}
+
+/**
+ * /auth/student 요청 본문에 실을 sub.
+ *
+ * 서버가 요청 sub를 무시하고 자체 UUID로 신원을 만들던 시절이 있어서, 기존 설치는
+ * 저장된 @auth_subject와 토큰 안의 신원이 서로 다른 값이다. 실제 편지함이 달린 신원은
+ * 토큰 쪽이므로, 저장된 토큰이 있으면 그 payload.sub를 그대로 다시 보낸다.
+ * 토큰이 없는 신규 설치에서만 @auth_subject를 쓴다.
+ */
+export async function resolveAuthSubject(): Promise {
+ const accessToken = await getStoredAccessToken();
+ const tokenSubject = accessToken ? getJwtSubject(accessToken) : null;
+
+ return tokenSubject ?? getOrCreateAuthSubject();
+}
diff --git a/services/auth-token.service.ts b/services/auth-token.service.ts
index 922f69c..1f05fcd 100644
--- a/services/auth-token.service.ts
+++ b/services/auth-token.service.ts
@@ -1,5 +1,5 @@
import { publicApi } from './api';
-import { getOrCreateAuthSubject, getStoredAccessToken, saveAccessToken } from './auth-token-storage';
+import { getStoredAccessToken, resolveAuthSubject, saveAccessToken } from './auth-token-storage';
type IssueAccessTokenResponse =
| {
@@ -46,7 +46,7 @@ function extractAccessToken(response: IssueAccessTokenResponse): string | null {
export async function issueAccessToken(): Promise {
const payload: IssueAccessTokenPayload = {
- sub: await getOrCreateAuthSubject(),
+ sub: await resolveAuthSubject(),
iat: Math.floor(Date.now() / 1000),
};