Skip to content

Commit

Permalink
Update yorkie-js-sdk to v0.5.5 (#407)
Browse files Browse the repository at this point in the history
* Update `yorkie-js-sdk` to `v0.5.5`

* Add missing `APITags`
  • Loading branch information
devleejb authored Nov 10, 2024
1 parent b5331f1 commit 3c2ec8e
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 76 deletions.
115 changes: 71 additions & 44 deletions backend/src/check/check.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from "@nestjs/common";
import { ForbiddenException, Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import * as moment from "moment";
import { PrismaService } from "src/db/prisma.service";
Expand Down Expand Up @@ -33,64 +33,91 @@ export class CheckService {
}

async checkYorkie(checkYorkieDto: CheckYorkieDto): Promise<CheckYorkieResponse> {
let reason = "";
let allowed = false;
const [type, token] = checkYorkieDto.token.split(":");

// In `ActivateClient`, `DeactivateClient` methods, the `checkYorkieDto.attributes` is empty.
if (
[YorkieMethod.ActivateClient, YorkieMethod.DeactivateClient].includes(
checkYorkieDto.method
)
) {
allowed = true;
reason = `Pass ${checkYorkieDto.method}`;
return {
allowed: true,
reason: `Pass ${checkYorkieDto.method} method`,
};
}

const { key: yorkieDocumentId } = checkYorkieDto.attributes?.[0];
if (type == "default") {
await this.checkDefaultAccessToken(yorkieDocumentId, token);
} else if (type == "share") {
await this.checkSharingAccessToken(yorkieDocumentId, token);
} else {
const { key: yorkieDocumentId } = checkYorkieDto.attributes?.[0];
if (type === "default") {
const { sub } = this.jwtService.verify<JwtPayload>(token);
throw new ForbiddenException({ allowed: false, reason: "Invalid token type" });
}

const res = await this.prismaService.document.findFirst({
select: {
id: true,
},
where: {
yorkieDocumentId,
workspace: {
userWorkspaceList: {
every: {
userId: sub,
},
},
return {
allowed: true,
reason: "Valid token",
};
}

private async checkDefaultAccessToken(
yorkieDocumentId: string,
accessToken: string
): Promise<string> {
let sub = "";
try {
sub = this.jwtService.verify<JwtPayload>(accessToken).sub;
} catch {
throw new UnauthorizedException({
allowed: false,
reason: "Token is expired or invalid",
});
}

const document = await this.prismaService.document.findFirst({
select: {
id: true,
},
where: {
yorkieDocumentId,
workspace: {
userWorkspaceList: {
every: {
userId: sub,
},
},
});
},
},
});

allowed = Boolean(res);
} else if (type === "share") {
const documentSharingToken =
await this.prismaService.documentSharingToken.findFirst({
where: {
token,
document: {
yorkieDocumentId,
},
},
});
if (!document) {
throw new ForbiddenException({
allowed: false,
reason: "User does not have access to the document",
});
}

return sub;
}

allowed = Boolean(documentSharingToken);
private async checkSharingAccessToken(yorkieDocumentId: string, accessToken: string) {
const documentSharingToken = await this.prismaService.documentSharingToken.findFirst({
where: {
token: accessToken,
document: {
yorkieDocumentId,
},
},
});

if (
documentSharingToken?.expiredAt &&
moment().isAfter(documentSharingToken?.expiredAt)
) {
allowed = false;
}
}
if (!documentSharingToken) {
throw new ForbiddenException({ allowed: false, reason: "Sharing token is invalid" });
}

return {
allowed,
reason,
};
if (documentSharingToken?.expiredAt && moment().isAfter(documentSharingToken?.expiredAt)) {
throw new ForbiddenException({ allowed: false, reason: "Sharing token is expired" });
}
}
}
3 changes: 2 additions & 1 deletion backend/src/files/files.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import {
Redirect,
StreamableFile,
} from "@nestjs/common";
import { ApiBody, ApiOkResponse, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { ApiBody, ApiOkResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Public } from "src/utils/decorators/auth.decorator";
import { CreateUploadPresignedUrlDto } from "./dto/create-upload-url.dto";
import { FilesService } from "./files.service";
import { CreateUploadPresignedUrlResponse } from "./types/create-upload-url-response.type";
import { ExportFileRequestBody, ExportFileResponse } from "./types/export-file.type";

@ApiTags("Files")
@Controller("files")
export class FilesController {
constructor(private filesService: FilesService) {}
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"refractor": "^4.8.1",
"validator": "^13.12.0",
"vite-plugin-package-version": "^1.1.0",
"yorkie-js-sdk": "0.5.4"
"yorkie-js-sdk": "0.5.5"
},
"devDependencies": {
"@sentry/vite-plugin": "^2.20.1",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/hooks/api/types/user.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ export class GetUserResponse extends User {}
export class UpdateUserRequest {
nickname: string;
}

export class RefreshTokenRequest {
refreshToken: string;
}

export class RefreshTokenResponse {
newAccessToken: string;
}
35 changes: 28 additions & 7 deletions frontend/src/hooks/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,40 @@ import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { logout, selectAuth, setAccessToken } from "../../store/authSlice";
import { User, setUserData } from "../../store/userSlice";
import { GetUserResponse, UpdateUserRequest } from "./types/user";
import {
GetUserResponse,
RefreshTokenRequest,
RefreshTokenResponse,
UpdateUserRequest,
} from "./types/user";

export const generateGetUserQueryKey = (accessToken: string) => {
return ["users", accessToken];
};

export const useRefreshTokenMutation = () => {
const dispatch = useDispatch();
const authStore = useSelector(selectAuth);

return useMutation({
mutationFn: async () => {
const response = await axios.post<RefreshTokenResponse>("/auth/refresh", {
refreshToken: authStore.refreshToken,
} as RefreshTokenRequest);

return response.data.newAccessToken;
},
onSuccess: (accessToken) => {
dispatch(setAccessToken(accessToken));
axios.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`;
},
});
};

export const useGetUserQuery = () => {
const dispatch = useDispatch();
const authStore = useSelector(selectAuth);
const { mutateAsync: mutateRefreshToken } = useRefreshTokenMutation();
const [axiosInterceptorAdded, setAxiosInterceptorAdded] = useState(false);

useEffect(() => {
Expand All @@ -26,11 +51,7 @@ export const useGetUserQuery = () => {
return Promise.reject(error);
} else {
error.config._retry = true;
const { refreshToken } = authStore;
const response = await axios.post("/auth/refresh", { refreshToken });
const newAccessToken = response.data.newAccessToken;
dispatch(setAccessToken(newAccessToken));
axios.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`;
const newAccessToken = await mutateRefreshToken();
error.config.headers["Authorization"] = `Bearer ${newAccessToken}`;
return axios(error.config);
}
Expand All @@ -45,7 +66,7 @@ export const useGetUserQuery = () => {
setAxiosInterceptorAdded(false);
axios.interceptors.response.eject(interceptor);
};
}, [authStore, dispatch]);
}, [authStore, dispatch, mutateRefreshToken]);

const query = useQuery({
queryKey: generateGetUserQueryKey(authStore.accessToken || ""),
Expand Down
52 changes: 34 additions & 18 deletions frontend/src/hooks/useYorkieDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as yorkie from "yorkie-js-sdk";
import { selectAuth } from "../store/authSlice";
import { CodePairDocType } from "../store/editorSlice";
import { YorkieCodeMirrorDocType, YorkieCodeMirrorPresenceType } from "../utils/yorkie/yorkieSync";
import { useRefreshTokenMutation } from "./api/user";

const YORKIE_API_ADDR = import.meta.env.VITE_YORKIE_API_ADDR;
const YORKIE_API_KEY = import.meta.env.VITE_YORKIE_API_KEY;
Expand All @@ -21,26 +22,42 @@ export const useYorkieDocument = (
const authStore = useSelector(selectAuth);
const [client, setClient] = useState<yorkie.Client | null>(null);
const [doc, setDoc] = useState<CodePairDocType | null>(null);
const { mutateAsync: mutateRefreshToken } = useRefreshTokenMutation();

const getYorkieToken = useCallback(
async (reason?: string) => {
const shareToken = searchParams.get("token");
let accessToken = authStore.accessToken;
const isShare = Boolean(shareToken);

if (reason) {
if (isShare) {
throw new Error("Cannot refresh token for shared documents");
} else {
try {
accessToken = await mutateRefreshToken();
} catch {
throw new Error("Failed to refresh token");
}
}
}

const getYorkieToken = useCallback(() => {
const shareToken = searchParams.get("token");
return shareToken ? `share:${shareToken}` : `default:${authStore.accessToken}`;
}, [authStore.accessToken, searchParams]);

const createYorkieClient = useCallback(
async (yorkieToken: string) => {
const syncLoopDuration = Number(searchParams.get("syncLoopDuration")) || 200;
const newClient = new yorkie.Client(YORKIE_API_ADDR, {
apiKey: YORKIE_API_KEY,
token: yorkieToken,
syncLoopDuration,
});
await newClient.activate();
return newClient;
return isShare ? `share:${shareToken}` : `default:${accessToken}`;
},
[searchParams]
[authStore.accessToken, mutateRefreshToken, searchParams]
);

const createYorkieClient = useCallback(async () => {
const syncLoopDuration = Number(searchParams.get("syncLoopDuration")) || 200;
const newClient = new yorkie.Client(YORKIE_API_ADDR, {
apiKey: YORKIE_API_KEY,
authTokenInjector: getYorkieToken,
syncLoopDuration,
});
await newClient.activate();
return newClient;
}, [getYorkieToken, searchParams]);

const createYorkieDocument = useCallback(
(client: yorkie.Client, yorkieDocumentId: string, presenceName: string) => {
const newDocument = new yorkie.Document<
Expand Down Expand Up @@ -76,8 +93,7 @@ export const useYorkieDocument = (

const initializeYorkie = async () => {
try {
const yorkieToken = getYorkieToken();
const newClient = await createYorkieClient(yorkieToken);
const newClient = await createYorkieClient();
const newDoc = await createYorkieDocument(
newClient,
yorkieDocumentId,
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3c2ec8e

Please sign in to comment.