From e75121435aabcdfa646c1ffbcdbaad75e17858fb Mon Sep 17 00:00:00 2001 From: dsinghvi Date: Wed, 23 Aug 2023 20:18:37 -0400 Subject: [PATCH 1/2] introduce fern docs --- .github/workflows/ci.yml | 2 +- fern/api/definition/api.yml | 6 - fern/api/definition/auth.yml | 54 -- fern/api/definition/clans.yml | 41 - fern/api/definition/classrooms.yml | 240 ----- fern/api/definition/commons.yml | 82 -- fern/api/definition/stats.yml | 57 -- fern/api/definition/users.yml | 326 ------- fern/apis/chinese/generators.yml | 1 + fern/apis/chinese/openapi/openapi.yml | 1025 ++++++++++++++++++++ fern/{api => apis/english}/generators.yml | 8 - fern/apis/english/openapi/openapi.yml | 1026 +++++++++++++++++++++ fern/assets/favicon.png | Bin 0 -> 14289 bytes fern/assets/logo.png | Bin 0 -> 31256 bytes fern/docs.yml | 39 + fern/fern.config.json | 2 +- fern/intro-ch.mdx | 31 + fern/intro-en.mdx | 58 ++ 18 files changed, 2182 insertions(+), 816 deletions(-) delete mode 100644 fern/api/definition/api.yml delete mode 100644 fern/api/definition/auth.yml delete mode 100644 fern/api/definition/clans.yml delete mode 100644 fern/api/definition/classrooms.yml delete mode 100644 fern/api/definition/commons.yml delete mode 100644 fern/api/definition/stats.yml delete mode 100644 fern/api/definition/users.yml create mode 100644 fern/apis/chinese/generators.yml create mode 100644 fern/apis/chinese/openapi/openapi.yml rename fern/{api => apis/english}/generators.yml (78%) create mode 100644 fern/apis/english/openapi/openapi.yml create mode 100644 fern/assets/favicon.png create mode 100644 fern/assets/logo.png create mode 100644 fern/docs.yml create mode 100644 fern/intro-ch.mdx create mode 100644 fern/intro-en.mdx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce0ea62..c17ac3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,4 +38,4 @@ jobs: POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} POSTMAN_WORKSPACE_ID: ${{ secrets.POSTMAN_WORKSPACE_ID }} PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - run: fern generate --group external --version ${{ github.ref_name }} --log-level debug + run: fern generate --apis english --group external --version ${{ github.ref_name }} --log-level debug diff --git a/fern/api/definition/api.yml b/fern/api/definition/api.yml deleted file mode 100644 index 6704bf8..0000000 --- a/fern/api/definition/api.yml +++ /dev/null @@ -1,6 +0,0 @@ -name: api -display-name: CodeCombat API -auth: basic -environments: - Production: https://codecombat.com/api -default-environment: Production \ No newline at end of file diff --git a/fern/api/definition/auth.yml b/fern/api/definition/auth.yml deleted file mode 100644 index 78e7345..0000000 --- a/fern/api/definition/auth.yml +++ /dev/null @@ -1,54 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json - -service: - display-name: UserService - auth: true - base-path: /auth/login-o-auth - endpoints: - get: - display-name: Login User - path: "" - method: GET - docs: > - Logs a [user](#users) in. - #### Example - ```javascript - url = - `https://codecombat.com/auth/login-o-auth?provider=${OAUTH_PROVIDER_ID}&accessToken=1234` - res.redirect(url) - // User is sent to this CodeCombat URL and assuming everything checks - out, - // is logged in and redirected to the home page. - ``` - In this example, we call your lookup URL (let's say, - `https://oauth.provider/user?t=<%= accessToken %>`) with the access - token (`1234`). The lookup URL returns `{ id: 'abcd' }` in this case. We - will match this `id` with the OAuthIdentity stored in the user - information in our db. If everything checks out, the user is logged in - and redirected to the home page. - request: - name: GetUserAuthRequest - query-parameters: - provider: - docs: Your OAuth Provider ID - type: string - accessToken: - docs: >- - Will be passed through your lookup URL to get the user ID. - Required if no `code`. - type: optional - code: - docs: >- - Will be passed to the OAuth token endpoint to get a token. - Required if no `accessToken`. - type: optional - redirect: - docs: >- - Override where the user will navigate to after successfully - logging in. - type: optional - errorRedirect: - docs: >- - If an error happens, redirects the user to this url, with at least - query parameters `code`, `errorName` and `message`. - type: optional diff --git a/fern/api/definition/clans.yml b/fern/api/definition/clans.yml deleted file mode 100644 index 92a9d6d..0000000 --- a/fern/api/definition/clans.yml +++ /dev/null @@ -1,41 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json - -imports: - commons: commons.yml - -service: - display-name: Clans Service - auth: true - base-path: /clan/{handle}/members - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - endpoints: - upsertClan: - display-name: Upsert User Into Clan - path: "" - method: PUT - docs: Upserts a user into the clan. - request: - name: UpsertClanRequest - body: - properties: - userId: - docs: The `_id` or `slug` of the user to add to the clan. - type: string - response: ClanResponse - -types: - ClanResponse: - docs: Subset of properties listed here - properties: - _id: optional - name: optional - displayName: optional - members: optional> - ownerID: optional - description: optional - type: optional - kind: optional - metadata: optional> diff --git a/fern/api/definition/classrooms.yml b/fern/api/definition/classrooms.yml deleted file mode 100644 index cf83012..0000000 --- a/fern/api/definition/classrooms.yml +++ /dev/null @@ -1,240 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json - -imports: - commons: commons.yml - -service: - display-name: ClassroomsService - auth: true - base-path: /classrooms - endpoints: - get: - display-name: Get Classroom Details - path: "" - method: GET - docs: Returns the classroom details for a class code. - request: - name: GetClassroomDetailsRequest - query-parameters: - code: - docs: The classroom's `code`. - type: string - retMemberLimit: - docs: limit the return number of members for the classroom - type: optional - response: commons.ClassroomResponseWithCode - - create: - display-name: Create a Classroom - path: "" - method: POST - docs: Creates a new empty `Classroom`. - request: - name: CreateClassroomRequest - body: - properties: - name: - docs: Name of the classroom - type: string - ownerID: commons.objectIdString - aceConfig: AceConfig - - upsertFromClassroom: - display-name: Upsert a User from Classroom - path: /{handle}/members - method: PUT - docs: Upserts a user into the classroom. - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: UpsertClassroomRequest - body: - properties: - code: - docs: The code for joining this classroom - type: string - userId: - docs: The `_id` or `slug` of the user to add to the class. - type: string - retMemberLimit: - docs: >- - limit the return number of members for the classroom, the - default value is 1000 - type: optional - response: commons.ClassroomResponse - - deleteUserFromClassroom: - display-name: Delete User from Classroom - path: /{handle}/members - method: DELETE - docs: Remove a user from the classroom. - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: DeleteUserFromClassroomRequest - body: - properties: - userId: - docs: The `_id` or `slug` of the user to remove from the class. - type: string - retMemberLimit: - docs: >- - limit the return number of members for the classroom, the - default value is 1000 - type: optional - response: commons.ClassroomResponse - - enrollUserInCourse: - display-name: Enroll a User in a Course - path: /{classroomHandle}/courses/{courseHandle}/enrolled - method: PUT - docs: | - Enrolls a user in a course in a classroom. - If the course is paid, user must have an active license. - User must be a member of the classroom. - path-parameters: - classroomHandle: - docs: The classroom's `_id`. - type: string - courseHandle: - docs: The course's `_id`. - type: string - request: - name: EnrollUserInCourseRequest - query-parameters: - retMemberLimit: - docs: >- - limit the return number of members for the classroom, the default - value is 1000 - type: optional - body: - properties: - userId: commons.objectIdString - response: commons.ClassroomResponse - - removeUserFromClassroom: - display-name: Remove a User from a Classroom - path: /{classroomHandle}/courses/{courseHandle}/remove-enrolled - method: PUT - docs: | - Removes an enrolled user from a course in a classroom. - path-parameters: - classroomHandle: - docs: The classroom's `_id`. - type: string - courseHandle: - docs: The course's `_id`. - type: string - request: - name: RemoveUserFromClassroomRequest - query-parameters: - retMemberLimit: - docs: >- - limit the return number of members for the classroom, the default - value is 1000 - type: optional - body: - properties: - userId: commons.objectIdString - response: commons.ClassroomResponse - - getMembersStats: - display-name: Get Members Stats for a Classroom - path: /{classroomHandle}/stats - method: GET - docs: | - Returns a list of all members stats for the classroom. - path-parameters: - classroomHandle: - docs: The classroom's `_id`. - type: string - request: - name: GetMembersStatsRequest - query-parameters: - project: - docs: > - If specified, include only the specified projection of returned - stats; else, return all stats. Format as a comma-separated list, - like `creator,playtime,state.complete`. - type: optional - memberLimit: - docs: >- - Limit the return member number. the default value is 10, and the - max value is 100 - type: optional - memberSkip: - docs: | - Skip the members that doesn't need to return, for pagination - type: optional - response: list - - getLevelSession: - display-name: Get Level Session - path: /{classroomHandle}/members/{memberHandle}/sessions - method: GET - docs: | - Returns a list of all levels played by the user for the classroom. - path-parameters: - classroomHandle: - docs: The classroom's `_id`. - type: string - memberHandle: - docs: The classroom member's `_id`. - type: string - response: list - -types: - LevelSessionResponse: - properties: - state: optional - level: optional - levelID: - docs: Level slug like `wakka-maul` - type: optional - creator: optional - playtime: - docs: Time played in seconds. - type: optional - changed: optional - created: optional - dateFirstCompleted: optional - submitted: - docs: For arenas. Whether or not the level has been added to the ladder. - type: optional - published: - docs: >- - For shareable projects. Whether or not the project has been shared - with classmates. - type: optional - - State: - properties: - complete: optional - - Level: - properties: - original: - docs: The id for the level. - type: optional - - AceConfig: - properties: - language: - docs: Programming language for the classroom - type: optional - - PlayStats: - properties: - gamesCompleted: optional - playtime: - docs: Total play time in seconds - type: optional - - MemberStat: - properties: - _id: optional - stats: optional diff --git a/fern/api/definition/commons.yml b/fern/api/definition/commons.yml deleted file mode 100644 index 7ff061e..0000000 --- a/fern/api/definition/commons.yml +++ /dev/null @@ -1,82 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json - -types: - UserResponse: - docs: Subset of properties listed here - properties: - _id: optional - email: optional - name: optional - slug: optional - role: optional - stats: optional - oAuthIdentities: optional> - subscription: optional - license: optional - - UserStats: - properties: - gamesCompleted: optional - concepts: optional> - playTime: - docs: Included only when specifically requested on the endpoint - type: optional - - AuthIdentity: - properties: - provider: optional - id: optional - - Subscription: - properties: - ends: optional - active: optional - - License: - properties: - ends: optional - active: optional - - objectIdString: string - - roleString: - docs: Usually either 'teacher' or 'student' - type: string - - datetimeString: string - - ClassroomResponseWithCode: - docs: Subset of properties listed here - properties: - _id: optional - name: optional - members: optional> - ownerID: optional - description: optional - code: optional - codeCamel: optional - courses: optional> - clanId: optional - - Course: - properties: - _id: optional - levels: optional>> - enrolled: optional> - instance_id: optional - - ClassroomResponse: - docs: Subset of properties listed here - properties: - _id: optional - name: - type: optional - docs: The name of the classroom - members: - type: optional> - docs: List of _ids of the student members of the classroom - ownerID: - type: optional - docs: The _id of the teacher owner of the classroom. - description: optional - courses: optional> diff --git a/fern/api/definition/stats.yml b/fern/api/definition/stats.yml deleted file mode 100644 index 4092c40..0000000 --- a/fern/api/definition/stats.yml +++ /dev/null @@ -1,57 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json - -service: - display-name: StatsService - auth: true - base-path: / - endpoints: - getPlaytimeStats: - display-name: Get Playtime Stats - path: /playtime-stats - method: GET - docs: Returns the playtime stats - request: - name: GetPlaytimeStats - query-parameters: - startDate: - docs: Earliest an included user was created - type: optional - endDate: - docs: Latest an included user was created - type: optional - country: - docs: Filter by country string - type: optional - response: PlaytimeStatsResponse - - getLicenseStats: - display-name: Get License Stats - path: /license-stats - method: GET - docs: Returns the license stats - response: LicenseStatsResponse - -types: - PlaytimeStatsResponse: - properties: - playTime: - docs: Total play time in seconds - type: optional - gamesPlayed: - docs: Number of levels played - type: optional - - LicenseStatsResponse: - properties: - licenseDaysGranted: - docs: Total number of license days granted - type: optional - licenseDaysUsed: - docs: Number of license days used - type: optional - licenseDaysRemaining: - docs: Number of license days remaining - type: optional - activeLicenses: - docs: Number of active/valid licenses - type: optional diff --git a/fern/api/definition/users.yml b/fern/api/definition/users.yml deleted file mode 100644 index 7dc79f1..0000000 --- a/fern/api/definition/users.yml +++ /dev/null @@ -1,326 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json - -imports: - commons: commons.yml - -service: - display-name: UsersService - auth: true - base-path: / - endpoints: - create: - display-name: Create User - path: /users - method: POST - docs: | - Creates a `User`. - #### Example - ```javascript - url = 'https://codecombat.com/api/users' - json = { email: 'an@email.com', name: 'Some Username', role: 'student' } - request.post({ url, json, auth }) - ``` - request: - name: CreateUserRequest - body: - properties: - name: string - email: string - role: - docs: >- - A `"student"` or `"teacher"`. If unset, a home user will be - created, unable to join classrooms. - type: optional - preferredLanguage: optional - heroConfig: optional - birthday: optional - - get: - display-name: Get User - path: /users/{handle} - method: GET - docs: Returns a `User`. - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: GetUserRequest - query-parameters: - includePlayTime: - docs: Set to non-empty string to include stats.playTime in response - type: optional - response: commons.UserResponse - - update: - display-name: Update User - path: /users/{handle} - method: PUT - docs: Modify name of a `User` - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: UpdateUserRequest - body: - properties: - name: - docs: Set to new name string - type: string - birthday: - docs: Set the birthday - type: optional - response: commons.UserResponse - - getClassrooms: - display-name: Get Classrooms By User - path: /users/{handle}/classrooms - method: GET - docs: >- - Returns a list of `Classrooms` this user is in (if a student) or owns - (if a teacher). - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: GetClassroomsRequest - query-parameters: - retMemberLimit: - docs: limit the return number of members for each classroom - type: optional - response: list - - getHero: - display-name: Get User Hero - path: /users/{handle}/hero-config - method: PUT - docs: Set the user's hero. - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: GetHeroRequest - body: - properties: - thangType: optional - response: commons.UserResponse - - setAceConfig: - path: /users/{handle}/ace-config - method: PUT - docs: >- - Set the user's aceConfig (the settings for the in-game Ace code editor), - such as whether to enable autocomplete. - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: SetAceConfig - body: - properties: - liveCompletion: - docs: >- - controls whether autocompletion snippets show up, the default - value is true - type: optional - behaviors: - docs: >- - controls whether things like automatic parenthesis and quote - completion happens, the default value is false - type: optional - language: - docs: >- - only for home users, should be one of ["python", "javascript", - "cpp", "lua", "coffeescript"] right now - type: optional - response: commons.UserResponse - - addOAuthIdentity: - display-name: Add OAuth2 Identity - path: /users/{handle}/o-auth-identities - method: POST - docs: > - Adds an OAuth2 identity to the user, so that they can be logged in with - that identity. You need to send the OAuth code or the access token to - this endpoint. - 1. If no access token is provided, it will use your OAuth2 token URL to - exchange the given code for an access token. - 1. Then it will use the access token (given by you, or received from - step 1) to look up the user on your service using the lookup URL, and - expects a JSON object in response with an `id` property. - 1. It will then save that user `id` to the user in our db as a new - OAuthIdentity. - #### Example - ```javascript - url = `https://codecombat.com/api/users/${userID}/o-auth-identities` - OAUTH_PROVIDER_ID = 'xyz' - json = { provider: OAUTH_PROVIDER_ID, accessToken: '1234' } - request.post({ url, json, auth}, (err, res) => { - console.log(res.body.oAuthIdentities) // [ { provider: 'xyx', id: 'abcd' } ] - }) - ``` - In this example, we call your lookup URL (let's say, - `https://oauth.provider/user?t=<%= accessToken %>`) with the access - token (`1234`). The lookup URL returns `{ id: 'abcd' }` in this case, - which we save to the user in our db. - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: AddOAuthIdentityRequest - body: - properties: - provider: - docs: Your OAuth Provider ID. - type: string - accessToken: - docs: >- - Will be passed through your lookup URL to get the user ID. - Required if no `code`. - type: optional - code: - docs: >- - Will be passed to the OAuth token endpoint to get a token. - Required if no `accessToken`. - type: optional - response: commons.UserResponse - - updateSubscription: - path: /users/{handle}/subscription - method: PUT - docs: | - Grants a user premium access to the "Home" version up to a certain time. - #### Example - ```javascript - url = `https://codecombat.com/api/users/${userID}/subscription` - json = { ends: new Date('2017-01-01').toISOString() } - request.put({ url, json, auth }, (err, res) => { - console.log(res.body.subscription) // { ends: '2017-01-01T00:00:00.000Z', active: true } - }) - ``` - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: UpdateSubscriptionRequest - body: - properties: - ends: datetime - response: commons.UserResponse - - shortenSubscription: - display-name: Shorten User Subscription - path: /users/{handle}/shorten-subscription - method: PUT - docs: > - If the user already has a premium access up to a certain time, this - shortens/revokes his/her premium access. - If the ends is less than or equal to the current time, it revokes the - subscription and sets the end date to be the current time, else it just - shortens the subscription. - #### Example - ```javascript - url = `https://codecombat.com/api/users/${userID}/shorten-subscription` - json = { ends: new Date().toISOString() } - request.put({ url, json, auth }, (err, res) => { - console.log(res.body.subscription.active) // false - }) - ``` - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: ShortenSubscriptionRequest - body: - properties: - ends: datetime - response: commons.UserResponse - - grantLicense: - display-name: Grant User License - path: /users/{handle}/license - method: PUT - docs: | - Grants a user access to the "Classroom" version up to a certain time. - Sets their role to "student". - #### Example - ```javascript - url = `https://codecombat.com/api/users/${userID}/license` - json = { ends: new Date('2017-01-01').toISOString() } - request.put({ url, json, auth }, (err, res) => { - console.log(res.body.license) // { ends: '2017-01-01T00:00:00.000Z', active: true } - }) - ``` - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: GrantLicenseRequest - body: - properties: - ends: datetime - response: commons.UserResponse - - shortenLicense: - display-name: Shorten User License - path: /users/{handle}/shorten-license - method: PUT - docs: > - If the user already has access to the "Classroom" version up to a - certain time, this shortens/revokes his/her access. - If the ends is less than or equal to the current time, it revokes the - enrollment and sets the end date to be the current time, else it just - shortens the enrollment. - #### Example - ```javascript - url = `https://codecombat.com/api/users/${userID}/shorten-license` - json = { ends: new Date().toISOString() } - request.put({ url, json, auth }, (err, res) => { - console.log(res.body.license.active) // false - }) - ``` - path-parameters: - handle: - docs: The document's `_id` or `slug`. - type: string - request: - name: ShortenLicenseRequest - body: - properties: - ends: datetime - response: commons.UserResponse - - #TODO feels like a chain of endpoint to endpoint actions - findUser: - display-name: Search for User - path: /user-lookup/{property}/{value} - method: GET - docs: Redirects to `/users/{handle}` given a unique, identifying property - path-parameters: - property: - docs: The property to lookup by. May either be `"israel-id"` or `"name"`. - type: string - value: - docs: The value to be looked up. - type: string - -types: - UserRole: - docs: >- - A `"student"` or `"teacher"`. If unset, a home user will be created, - unable to join classrooms. - enum: - - student - - teacher - - HeroConfig: - properties: - thangType: optional diff --git a/fern/apis/chinese/generators.yml b/fern/apis/chinese/generators.yml new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/fern/apis/chinese/generators.yml @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/fern/apis/chinese/openapi/openapi.yml b/fern/apis/chinese/openapi/openapi.yml new file mode 100644 index 0000000..0f02ef8 --- /dev/null +++ b/fern/apis/chinese/openapi/openapi.yml @@ -0,0 +1,1025 @@ +swagger: "2.0" +info: + version: "0.0.0" + title: CodeCombat开发者接口 + description: | + ## 基本情况 + + * 开发者在Node/Express服务器上安装了以下 [请求](https://github.com/request/request),示例采用JavaScript形式。 + * 请求和反馈形式为JSON。 + * API接口是创建或者参考的基础资源。因此,举例来说,所有的开发路径(routes)是以'/api/users'开始,返回[User](#users)开发资源。 + + ## 客户设置 + 目前我们没有让您创建或者设置您自己的客户级开发者接口(API Client)或者OAuth提供者(OAuth Privider)信息。请直接联系我们启动您的开发进程。 + + ## 用户授权 + 系统必须通过基础HTTP授权(Basic HTTP Authentication)来启动API路径。在我们的系统中创建你的API客户(API_CLIENT)之后,你会获得一个用户名(CLIENT_ID)和密码(CLIENT_SECRET)。对于每一个API请求,请提供你的姓名(CLIENT_ID)和密码(CLIENT_SECRET)。 + + ```javascript + url = 'https://koudashijie.com/api/users' + json = { name: 'A username' } + auth = { name: CLIENT_ID, pass: CLIENT_SECRET } + request.get({ url, json, auth }, (err, res) => console.log(res.statusCode, res.body)) + ``` + + ## 用户授权 + 通过您的服务在CodeCombat平台上认证一个用户,您需要采用以下oAuth 2流程. CodeCombat作为一个客户,您的服务作为服务提供者。首先,您需要提供一个可信的查找网址(lookup URL 或者 token URL),来进行创建设置(参见以上客户创建说明),这个创建账户和登陆的过程如以下所示: + + 1. **创建用户** 使用[POST/api/users](#users/post_users). + 1. **把CodeCombat用户和一个oAuth身份对接** 使用[POST/api/users/:handle/o-auth-identities](#users/post_users__handle__o_auth_identities). + 你可以使用代码或者准入指令(access token)来启动这个API嵌入。如果你没有获得准入指令(access token),我们会用这个URL指令来替换原有的代码来获得一个准入指令(access token)。 + 然后我们用准入指令(access token)来启动查找网址(lookup URL),以此从你的系统中获得用户信息(`id`),这些信息已被储存在我们的用户数据库中。 + 1. **用户登陆** 重新引导用户到以下路径 [/auth/login-o-auth](#auth/get_auth_login_o_auth). + 你可以用这个代码/准入指令(access token)来call API, 然后我们会通过类似于第二步中的步骤来获得用户信息。 + 最后,我们将数据库在第二步中获得的信息与这个信息进行匹配,如果匹配成功,用户即可完成登录并被引导到主页。 + + 这里还有一个描述以上过程的 [实例](https://s3.amazonaws.com/files.codecombat.com/codecombat_oauth_example.tar.gz),以便理解。 同时,你也可以参考这个 [图表](https://s3.amazonaws.com/files.codecombat.com/Example_OAuth_Flow.png)。 + +host: koudashijie.com +basePath: /api +schemes: + - https +consumes: + - application/json +produces: + - application/json + +paths: + /auth/login-o-auth: + get: + summary: 登录用户 + tags: + - auth + operationId: loginOauth + description: | + 用 [user](#users) 登陆. + + 在这个示例中,我们用准入指令(access token)(`1234`)来call你的查找的URL(lookup URL)(比方说,是 ‘https://oauth.provider/user?t=<%=accessToken%>` 。在这个例子中,返回的查找URL(lookup URL)是 `{ id: 'abcd' }`。我们将数据库中储存的OAuthIdentity用户信息与这个`id`匹配。如果匹配成功,用户即可登录并被引导到主页。 + + parameters : + - name: provider + in: query + required: true + type: string + description: 你的授权(oAuth)提供者ID + - name: accessToken + in: query + type: string + description: 通过传输你的查询网址以便于拿到用户ID。如果没有“代码”这一环节是必须项。 + - name: code + type: string + in: query + description: 通过传输到授权指令的终端以便于拿到指令。如果没有“代码”这一环节是必须项。如果没有`accessToken`(“访问指令”)这一环节是必须项。 + - name: redirect + type: string + in: query + description: 登陆成功后,用户导航路径会被清除。 + - name: errorRedirect + type: string + in: query + description: 如果错误出现,将用户重置于这个网址,请求参数至少需要`code`(“代码”)、`errorName`(“错误名称”)和`message`(“消息”)等字段。 + responses: + "302": + description: '用户成功登录以后,将用户重置于登陆页面。' + + /users: + post: + summary: 创建用户 + tags: + - users + operationId: users-create + description: | + 创建一个`User`(“用户”) + + parameters: + - name: user + in: body + schema: + type: object + required: + - name + - email + properties: + name: + type: string + email: + type: string + role: + type: string + description: | + `"student"` 或者 `"teacher"` + enum: + - student + - teacher + preferredLanguage: + type: string + heroConfig: + type: object + properties: + thangType: + $ref: '#/definitions/objectIdString' + birthday: + type: string + responses: + "201": + description: '已经创建的用户' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}: + get: + summary: 获取用户 + tags: + - users + operationId: users-get + description: 返回一个`User`(“用户”)。 + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: includePlayTime + in: query + description: "设置非空字符串包括反馈函数stats.playTime" + required: false + type: string + responses: + "200": + description: '需求用户' + schema: + $ref: '#/definitions/UserResponse' + + put: + tags: + - users + operationId: users-update + description: 修改一个 `User`("用户") 的姓名 + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: user + in: body + required: true + schema: + type: object + properties: + name: + type: string + description: '设置一个新姓名' + birthday: + type: string + description: '设置生日' + responses: + "200": + description: '影响的用户' + schema: + $ref: '#/definitions/UserResponse' + + + + /users/{handle}/classrooms: + get: + summary: 按用户获取教室 + tags: + - users + operationId: users-get-classrooms + description: 返回`Clasrooms`(“教室”)清单,这个用户(如果是学生)在教室里或者是教室的主人(如果是老师)。 + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: retMemberLimit + in: query + description: "设置返回的教室中显示的学生数量" + required: false + type: number + responses: + "200": + description: '教室需求' + schema: + type: array + items: + $ref: '#/definitions/ClassroomResponseWithCode' + + /users/{handle}/hero-config: + put: + summary: 获取用户英雄 + tags: + - users + operationId: users-set-hero + description: 设置用户的英雄。 + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: heroConfig + in: body + schema: + type: object + properties: + thangType: + $ref: '#/definitions/objectIdString' + responses: + "200": + description: '受影响的客户' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/ace-config: + put: + summary: 设置用户的代码配置 + tags: + - users + operationId: users-set-ace-config + description: 设置用户的代码配置 ( 指用户在编辑器中可以修改的设置 ) , 如代码补全等. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: aceConfig + in: body + schema: + type: object + properties: + liveCompletion: + type: boolean + description: '控制代码自动补全提示是否出现, 默认值是 true' + behaviors: + type: boolean + description: '控制括号, 引号是否自动补齐, 默认值是 false' + language: + type: string + description: '控制个人账号(学生账号无效)使用的语言, 目前仅支持 python, javascript, cpp, lua, coffeescript' + responses: + "200": + description: '受影响的客户' + schema: + $ref: '#/definitions/UserResponse' + + + /users/{handle}/o-auth-identities: + post: + summary: 将一个OAuth2身份 + tags: + - users + operationId: users-link-oauth-identity + description: | + 将一个OAuth2身份(OAuth2 identity)加给用户,用户可以以此身份登录。你需要将一个OAuth代码或者准入指令(access token)发送到这个端口。 + + 1. 如果没有获得准入指令(access token)的话,系统会使用OAuth2指令URL(OAuth2 token URL)来交换获得的代码,以此获得准入指令(access token)。 + 2. 然后系统会在你的服务上使用查找URL(lookup URL)爱你的通过准入指令(access token) (改指令可能是你之前给予的,或者是在第一步中获得的)来查找这名用户。 并预计获得一个回应一个`id`属性的JSON对象。 + 3. 之后系统将保存用户`id`到我们的用户数据库中,作为一个新的OAuthIdentity. + + 在这个示例中, 我们用准入指令(access token)(`1234`)来call你的查找网址(lookup URL)(比方说是 `https://oauth.provider/user?t=<%= accessToken %>`)然后查找网址(lookup URL)返回`{ id: 'abcd' }`,我们已经将这个用户储存到我们的数据库中了。 + + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: oAuthIdentity + in: body + required: true + schema: + type: object + required: + - provider + properties: + provider: + type: string + description: 你的授权提供者 ID (OAuth Provider ID)。 + accessToken: + type: string + description: 将传输查询网址得到用户ID。如果没有`code`(“代码”)需要这一项。 + code: + type: string + description: 将传输授权指令终点得到令牌。如果没有`accessToken`(“访问令牌”)需要这一项。 + + responses: + "200": + description: '受影响用户' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/subscription: + put: + summary: 授予订阅 + tags: + - users + operationId: users-grant-premium-subscription + description: | + 在一段时间内授权用户访问“主页”版本的高级访问权限。 + + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: subscription + in: body + required: true + schema: + type: object + required: + - ends + properties: + ends: + $ref: '#/definitions/datetimeString' + responses: + "200": + description: '受影响用户' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/shorten-subscription: + put: + summary: 撤销他的高级访问权限 + tags: + - users + operationId: users-shorten-subscription + description: | + 如果用户在一段时间已经有了高级访问权限,这将缩短/撤销他的高级访问权限。 + 如果结束时间少于或者等于现在时间,将撤销订阅权限并将结束时间设置为现在时间,否则将缩短订阅时间。 + + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: subscription + in: body + required: true + schema: + type: object + required: + - ends + properties: + ends: + $ref: '#/definitions/datetimeString' + responses: + "200": + description: '受影响客户' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/license: + put: + summary: 格兰特“课堂”版 + tags: + - users + operationId: users-grant-license + description: | + 在一段时间内给予用户使用“教室”版本的权限。定义他们的角色为`"student"`(“学生”)。 + + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: license + in: body + required: true + schema: + type: object + required: + - ends + properties: + ends: + $ref: '#/definitions/datetimeString' + responses: + "200": + description: '受影响用户' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/shorten-license: + put: + summary: 缩短用户许可 + tags: + - users + operationId: users-shorten-license + description: | + 如果用户在一定时间内已经登陆“教室”版本,这将缩短或撤销他的权限。 + 如果结束比现在时间短或者和现在时间相同, 这将撤销登陆,并将结束时间设置为当前时间,否则这将缩短注册时间。 + + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: license + in: body + required: true + schema: + type: object + required: + - ends + properties: + ends: + $ref: '#/definitions/datetimeString' + responses: + "200": + description: '受影响客户' + schema: + $ref: '#/definitions/UserResponse' + + /clan/{handle}/members: + put: + summary: 将用户添加到部落 + tags: + - clans + operationId: clans-upsert-member + description: 在部落中加入一个用户 + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: member + in: body + required: true + schema: + type: object + required: + - userId + properties: + userId: + description: "使用字符串 `_id` 或 `slug` 来识别要加入部落的用户." + type: string + responses: + "200": + description: '返回加入了新成员的部落' + schema: + $ref: '#/definitions/ClanResponse' + + + /classrooms: + post: + summary: 创建一个教室 + tags: + - classrooms + operationId: classrooms-create + description: 创建一个新的空“教室” + parameters: + - name: classroom + in: body + required: true + schema: + type: object + required: + - name + - ownerID + - aceConfig + properties: + name: + type: string + description: 教室名称 + ownerID: + $ref: '#/definitions/objectIdString' + description: 教师 `_id` 是指哪间教室被创建了 + aceConfig: + type: object + properties: + language: + type: string + description: 适合教室的编程语言 + responses: + "201": + description: '创建的教室' + schema: + $ref: '#/definitions/ClassroomResponseWithCode' + + get: + summary: 获得一间教室 + tags: + - classrooms + operationId: classrooms-get + description: 教室模式之返回教室细节. + parameters: + - name: code + in: query + type: string + required: true + description: 教室 `code`(”代码”) + - name: retMemberLimit + in: query + description: "设置返回的教室中显示的学生数量" + required: false + type: number + responses: + "200": + description: '教室细节' + schema: + $ref: '#/definitions/ClassroomResponseWithCode' + + /classrooms/{handle}/members: + put: + summary: 添加会员 + tags: + - classrooms + operationId: classrooms-upsert-member + description: 在教室插入一个用户. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: member + in: body + required: true + schema: + type: object + required: + - code + - userId + properties: + code: + type: string + description: "加入教室需使用代码" + userId: + description: "使用字符串 `_id` 或`slug` 用来在教室中增加用户." + type: string + retMemberLimit: + description: "设置返回的教室中显示的学生数量, 默认值为 1000" + type: number + responses: + "200": + description: '教室中新增新成员' + schema: + $ref: '#/definitions/ClassroomResponse' + + delete: + tags: + - classrooms + summary: 从教室中移出一个学生 + operationId: classrooms-remove-member + description: 从教室中移出一个学生 + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: member + in: body + required: true + schema: + type: object + required: + - userId + properties: + userId: + description: "使用字符串 `_id` 或`slug` 用来从教室中移出用户." + type: string + retMemberLimit: + description: "设置返回的教室中显示的学生数量, 默认值为 1000" + type: number + responses: + "200": + description: '移出了学生的教室' + schema: + $ref: '#/definitions/ClassroomResponse' + + + /classrooms/{classroomHandle}/courses/{courseHandle}/enrolled: + put: + summary: 在课程中注册用户 + tags: + - classrooms + operationId: classrooms-enroll-user-in-course + description: | + 在一个教室里的一门课程上注册一个用户。 + 如果课程是付费课程,用户需要一个有效的订阅许可。 + 用户必须是教室中的一个成员。 + parameters: + - name: classroomHandle + in: path + type: string + required: true + description: 教室 `_id`. + - name: courseHandle + in: path + type: string + required: true + description: 课程 `_id`. + - name: userId + description: "用户的`_id`或者`slug`需要添加到教室中。" + in: body + required: true + schema: + type: object + required: + - userId + properties: + userId: + $ref: '#/definitions/objectIdString' + - name: retMemberLimit + in: query + description: "设置返回的教室中显示的学生数量, 默认值为 1000" + required: false + type: number + + responses: + "200": + description: '教室并且用户加入其中。' + schema: + $ref: '#/definitions/ClassroomResponse' + + /classrooms/{classroomHandle}/courses/{courseHandle}/remove-enrolled: + put: + tags: + - classrooms + summary: 删除注册用户 + operationId: classrooms-remove-enrolled-user + description: | + 将用户从注册的课程和教室中移除。 + parameters: + - name: classroomHandle + in: path + type: string + required: true + description: 教室`_id`. + - name: courseHandle + in: path + type: string + required: true + description: 课程`_id`. + - name: userId + description: "用户的`_id`或者`slug`。" + in: body + required: true + schema: + type: object + required: + - userId + properties: + userId: + $ref: '#/definitions/objectIdString' + - name: retMemberLimit + in: query + description: "设置返回的教室中显示的学生数量, 默认值为 1000" + required: false + type: number + responses: + "200": + description: '教室中,将用户移除课程。' + schema: + $ref: '#/definitions/ClassroomResponse' + + /classrooms/{classroomHandle}/stats: + get: + summary: 获取会员统计信息 + tags: + - classrooms + operationId: classrooms-get-members-stats + description: | + 返回教室中所有学生的相关数据 + parameters: + - name: classroomHandle + in: path + type: string + required: true + description: 教室 `_id`. + - name: project + in: query + type: string + description: | + 限制返回的数据内容, 由以下字符串按逗号连接组成: ["creator", "playtime", "state.complete"...] + required: false + - name: memberLimit + in: query + type: number + description: '返回值中的学生数量, 默认为 10, 最大值为 100' + required: false + - name: memberSkip + in: query + type: number + description: '返回值中跳过的学生数量, 和 memberLimit 共同起到分页作用' + required: false + + responses: + "200": + description: | + 返回教室中所有学生的相关数据 + schema: + type: array + items: + type: object + properties: + _id: + $ref: '#/definitions/objectIdString' + stats: + type: object + properties: + gamesCompleted: + type: number + playtime: + type: number + description: "游戏时间(秒)" + + + /classrooms/{classroomHandle}/members/{memberHandle}/sessions: + get: + summary: 玩过关卡 + tags: + - classrooms + operationId: classrooms-get-levels-played + description: | + 返回这个教室中用户所玩得所有关卡. + parameters: + - name: classroomHandle + in: path + type: string + required: true + description: 教室 `_id`. + - name: memberHandle + in: path + type: string + required: true + description: 教室成员 `_id`. + responses: + "200": + description: '教室的用户已经注册.' + schema: + type: array + items: + $ref: '#/definitions/LevelSessionResponse' + + /user-lookup/{property}/{value}: + get: + summary: 搜索用户 + tags: + - users + operationId: users-lookup + description: 采用唯一、可识别属性重新定向到`/users/{handle}` + parameters: + - name: property + in: path + type: string + required: true + description: 查找属性对应字符串,也许是 `"israel-id"` 或者 `"name"`. + - name: value + in: path + type: string + required: true + description: 查找的值. + responses: + "301": + description: "重新定位用户的资源到`/users/{handle}`" + + /playtime-stats: + get: + summary: 获取游戏时间统计数据 + tags: + - stats + operationId: stats-get-playtime-stats + description: 返回游戏时间统计 + parameters: + - name: startDate + in: query + description: "早先有一个已经注册的用户重新创建" + required: false + type: string + - name: endDate + in: query + description: "早先有一个已经注册的用户重新创建" + required: false + type: string + - name: country + in: query + description: "按照国家字符串进行过滤" + required: false + type: string + + responses: + "200": + description: "反馈所有自有用户的游戏时间统计." + schema: + $ref: '#/definitions/PlaytimeStatsResponse' + + /license-stats: + get: + summary: 获取许可证统计信息 + tags: + - stats + operationId: stats-get-license-stats + description: 返回订阅数据统计 + + responses: + "200": + description: "对于教室/家庭订阅许可返回订阅统计数据。" + schema: + $ref: '#/definitions/LicenseStatsResponse' + +parameters: + handlePathParameter: + name: handle + in: path + type: string + required: true + description: 文件中的 `_id` 或者 `slug`. + +tags: + - name: "users" + +securityDefinitions: + basicAuth: + type: basic + description: HTTP基础授权。我们会提供你的客户ID(`CLIENT_ID`)和私密(`CLIENT_SECRET`)文件给你 + +security: + - basicAuth: [] + +definitions: + roleString: + type: string + description: 通常这是指`"teacher"`("老师")或者`"student"`("学生") + + datetimeString: + type: string + pattern: /^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}\.\d{3}Z$/ + + objectIdString: + type: string + pattern: /^[0-9a-f]{24}$/ + + UserResponse: + type: object + title: 'UserResponse (用户反馈结果)' + description: '这里所列类别的子选项' + properties: + _id: + $ref: '#/definitions/objectIdString' + email: + type: string + name: + type: string + slug: + type: string + role: + $ref: '#/definitions/roleString' + stats: + type: object + properties: + gamesCompleted: + type: number + concepts: + type: object + additionalProperties: + type: number + playTime: + type: number + description: '只有在终点询问的时候才包含进去' + oAuthIdentities: + type: array + items: + type: object + properties: + provider: + type: string + id: + type: string + subscription: + type: object + properties: + ends: + $ref: '#/definitions/datetimeString' + active: + type: 'boolean' + license: + type: object + properties: + ends: + $ref: '#/definitions/datetimeString' + active: + type: 'boolean' + + ClassroomResponse: + type: object + title: 'ClassroomResponse (教室反馈结果)' + description: '这里列出特征的子集' + properties: + _id: + $ref: '#/definitions/objectIdString' + name: + type: string + members: + type: array + items: + $ref: '#/definitions/objectIdString' + ownerID: + $ref: '#/definitions/objectIdString' + description: + type: string + courses: + type: array + items: + type: object + properties: + _id: + $ref: '#/definitions/objectIdString' + levels: + type: array + items: + type: object + enrolled: + type: array + items: + $ref: '#/definitions/objectIdString' + instance_id: + $ref: '#/definitions/objectIdString' + + ClassroomResponseWithCode: + type: object + title: 'ClassroomResponseWithCode (教室反馈结果代码)' + description: '这里列出属性的子集' + properties: + _id: + $ref: '#/definitions/objectIdString' + name: + type: string + members: + type: array + items: + $ref: '#/definitions/objectIdString' + ownerID: + $ref: '#/definitions/objectIdString' + description: + type: string + code: + type: string + codeCamel: + type: string + courses: + type: array + items: + type: object + properties: + _id: + $ref: '#/definitions/objectIdString' + levels: + type: array + items: + type: object + enrolled: + type: array + items: + $ref: '#/definitions/objectIdString' + instance_id: + $ref: '#/definitions/objectIdString' + clanId: + $ref: '#/definitions/objectIdString' + + + PlaytimeStatsResponse: + type: object + properties: + playTime: + type: number + description: "以秒统计游戏使用时间" + gamesPlayed: + type: number + description: "所玩的级别" + + LicenseStatsResponse: + type: object + properties: + licenseDaysGranted: + type: number + description: "授权订阅天数总数量" + licenseDaysUsed: + type: number + description: "订阅许可天数已使用数量" + licenseDaysRemaining: + type: number + description: "订阅许可天数剩余数量" + activeLicenses: + type: number + description: "活跃/有效订阅许可数量" + + LevelSessionResponse: + type: object + properties: + state: + type: object + properties: + complete: + type: boolean + level: + type: object + properties: + original: + type: string + description: '这个`id`代表这个水平' + levelID: + type: 'string' + description: '关卡名,如 wakka-maul' + creator: + type: 'string' + $ref: '#/definitions/objectIdString' + playtime: + type: 'integer' + description: '以秒为单位游戏所玩时间' + changed: + $ref: '#/definitions/datetimeString' + created: + $ref: '#/definitions/datetimeString' + dateFirstCompleted: + $ref: '#/definitions/datetimeString' + submitted: + type: 'boolean' + description: '对于竞技场来说,这一关是否已经加入进阶关卡。' + published: + type: 'boolean' + description: '分享的项目。这些项目是否已经分享给了同学。' + + ClanResponse: + type: object + title: 'ClanResponse' + description: '这里列出特征的子集' + properties: + _id: + $ref: '#/definitions/objectIdString' + name: + type: string + displayName: + type: string + members: + type: array + items: + $ref: '#/definitions/objectIdString' + ownerID: + $ref: '#/definitions/objectIdString' + description: + type: string + type: + type: string + kind: + type: string + metadata: + type: object \ No newline at end of file diff --git a/fern/api/generators.yml b/fern/apis/english/generators.yml similarity index 78% rename from fern/api/generators.yml rename to fern/apis/english/generators.yml index 916123c..9859a9f 100644 --- a/fern/api/generators.yml +++ b/fern/apis/english/generators.yml @@ -21,14 +21,6 @@ groups: repository: codecombat/codecombat-python config: client_class_name: CodeCombat - - name: fernapi/fern-openapi - version: 0.0.19 - github: - repository: codecombat/codecombat-openapi - - name: fernapi/fern-openapi - version: 0.0.19 - github: - repository: codecombat/docs - name: fernapi/fern-postman version: 0.0.34 output: diff --git a/fern/apis/english/openapi/openapi.yml b/fern/apis/english/openapi/openapi.yml new file mode 100644 index 0000000..23ad602 --- /dev/null +++ b/fern/apis/english/openapi/openapi.yml @@ -0,0 +1,1026 @@ +swagger: "2.0" +info: + version: "0.0.0" + title: CodeCombat API + description: | + ## Basics + + * Examples are in JavaScript on a Node/Express server with [request](https://github.com/request/request) installed. + * Request and responses are in JSON. + * API responses are the base resource being created/referenced. So, for example, all routes starting with `/api/users` return [User](#users) resources. + + ## Client Setup + + We currently do not have a way for you to create or set up your own API Client or OAuth Provider information. Please contact us directly to get started. + + ## Client Authentication + + API routes must be called with Basic HTTP Authentication. You will receive a username (CLIENT_ID) and password (CLIENT_SECRET) upon creation of your API Client in our system. Provide those credentials with each API request. + + ```javascript + url = 'https://codecombat.com/api/users' + json = { name: 'A username' } + auth = { name: CLIENT_ID, pass: CLIENT_SECRET } + request.get({ url, json, auth }, (err, res) => console.log(res.statusCode, res.body)) + ``` + + We strongly recommend using a secrets manager for storing your client secret. Plain text files like dotenv lead to accidental, costly leaks. Use [Doppler](https://doppler.com/) for a developer-friendly experience. AWS and Google Cloud have native solutions as well. + + ## User Authentication + + To authenticate a user on CodeCombat through your service, you will need to use the below OAuth 2 process. CodeCombat will act as the client, and your service will act as the provider. + First, you will need to provide a trusted lookup URL and/or a token URL for the setup(See Client Setup above). Then the process from user account creation to log in will look like this: + + 1. **Create the user** using [POST /api/users](#users/post_users). + 1. **Link the CodeCombat user to an OAuth identity** using [POST /api/users/:handle/o-auth-identities](#users/post_users__handle__o_auth_identities). + You can call this API with a code or an access token. If no access token is given, we will use the token URL to exchange the given code for an access token. + Then we call the lookup URL with the access token to receive the user information (`id`) from your system which is saved to the user in our db. + 1. **Log the user in** by redirecting them to [/auth/login-o-auth](#auth/get_auth_login_o_auth). + You can call this API with the code/access token, and we will get the user information from your system similarly to step 2. + Finally, we match this information with what is stored in our database in step 2. If everything checks out, the user is logged in and redirected to the home page. + + There is also a [concrete example](https://s3.amazonaws.com/files.codecombat.com/codecombat_oauth_example.tar.gz) depicting the above process for better understanding. You can also refer to this [diagram](https://s3.amazonaws.com/files.codecombat.com/Example_OAuth_Flow.png). + +host: codecombat.com +basePath: /api +schemes: + - https +consumes: + - application/json +produces: + - application/json + +paths: + /auth/login-o-auth: + get: + summary: Login User + tags: + - auth + operationId: loginOauth + description: | + Logs a user in. + + In this example, we call your lookup URL (let's say, `https://oauth.provider/user?t=<%= accessToken %>`) with the access token (`1234`). The lookup URL returns `{ id: 'abcd' }` in this case. We will match this `id` with the OAuthIdentity stored in the user information in our db. If everything checks out, the user is logged in and redirected to the home page. + parameters: + - name: provider + in: query + required: true + type: string + description: Your OAuth Provider ID + - name: accessToken + in: query + type: string + description: Will be passed through your lookup URL to get the user ID. Required if no `code`. + - name: code + type: string + in: query + description: Will be passed to the OAuth token endpoint to get a token. Required if no `accessToken`. + - name: redirect + type: string + in: query + description: Override where the user will navigate to after successfully logging in. + - name: errorRedirect + type: string + in: query + description: If an error happens, redirects the user to this url, with at least query parameters `code`, `errorName` and `message`. + responses: + "302": + description: 'Redirects the user to a landing page after having logged them in.' + + /users: + post: + summary: Create User + tags: + - users + operationId: users-create + description: | + Creates a `User`. + parameters: + - name: user + in: body + schema: + type: object + required: + - name + - email + properties: + name: + type: string + email: + type: string + role: + type: string + description: | + `"student"` or `"teacher"`. If unset, a home user will be created, unable to join classrooms. + enum: + - student + - teacher + preferredLanguage: + type: string + heroConfig: + type: object + properties: + thangType: + $ref: '#/definitions/objectIdString' + birthday: + type: string + responses: + "201": + description: 'The created user' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}: + get: + summary: Get User + tags: + - users + operationId: users-get + description: Returns a `User`. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: includePlayTime + in: query + description: "Set to non-empty string to include stats.playTime in response" + required: false + type: string + responses: + "200": + description: 'The requested user' + schema: + $ref: '#/definitions/UserResponse' + + put: + summary: Update User + tags: + - users + operationId: users-update + description: Modify name of a `User` + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: user + in: body + required: true + schema: + type: object + required: + - name + properties: + name: + type: string + description: 'Set to new name string' + birthday: + type: string + description: 'Set the birthday' + responses: + "200": + description: 'The affected user' + schema: + $ref: '#/definitions/UserResponse' + + + /users/{handle}/classrooms: + get: + summary: Get Classrooms By User + tags: + - users + operationId: users-get-classrooms + description: Returns a list of `Classrooms` this user is in (if a student) or owns (if a teacher). + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: retMemberLimit + in: query + description: "limit the return number of members for each classroom" + required: false + type: number + responses: + "200": + description: 'The requested classrooms' + schema: + type: array + items: + $ref: '#/definitions/ClassroomResponseWithCode' + + /users/{handle}/hero-config: + put: + summary: Get User Hero + tags: + - users + operationId: users-set-hero + description: Set the user's hero. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: heroConfig + in: body + schema: + type: object + properties: + thangType: + $ref: '#/definitions/objectIdString' + responses: + "200": + description: 'The affected user' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/ace-config: + put: + summary: Put Ace Config + tags: + - users + operationId: users-set-ace-config + description: Set the user's aceConfig (the settings for the in-game Ace code editor), such as whether to enable autocomplete. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: aceConfig + in: body + schema: + type: object + properties: + liveCompletion: + type: boolean + required: false + description: 'controls whether autocompletion snippets show up, the default value is true' + behaviors: + type: boolean + required: false + description: 'controls whether things like automatic parenthesis and quote completion happens, the default value is false' + language: + type: string + required: false + description: 'only for home users, should be one of ["python", "javascript", "cpp", "lua", "coffeescript"] right now' + responses: + "200": + description: 'The affected user' + schema: + $ref: '#/definitions/UserResponse' + + + /users/{handle}/o-auth-identities: + post: + tags: + - users + summary: Add Oauth2 Identity + description: | + Adds an OAuth2 identity to the user, so that they can be logged in with that identity. You need to send the OAuth code or the access token to this endpoint. + + 1. If no access token is provided, it will use your OAuth2 token URL to exchange the given code for an access token. + 2. Then it will use the access token (given by you, or received from step 1) to look up the user on your service using the lookup URL, and expects a JSON object in response with an `id` property. + 3. It will then save that user `id` to the user in our db as a new OAuthIdentity. + + In this example, we call your lookup URL (let's say, `https://oauth.provider/user?t=<%= accessToken %>`) with the access token (`1234`). The lookup URL returns `{ id: 'abcd' }` in this case, which we save to the user in our db. + + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: oAuthIdentity + in: body + required: true + schema: + type: object + required: + - provider + properties: + provider: + type: string + description: Your OAuth Provider ID. + accessToken: + type: string + description: Will be passed through your lookup URL to get the user ID. Required if no `code`. + code: + type: string + description: Will be passed to the OAuth token endpoint to get a token. Required if no `accessToken`. + + responses: + "200": + description: 'The affected user' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/subscription: + put: + tags: + - users + summary: Put Subscription + operationId: users-grant-premium-subscription + description: | + Grants a user premium access to the "Home" version up to a certain time. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: subscription + in: body + required: true + schema: + type: object + required: + - ends + properties: + ends: + $ref: '#/definitions/datetimeString' + responses: + "200": + description: 'The affected user' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/shorten-subscription: + put: + tags: + - users + summary: Shorten User Subscription + operationId: users-shorten-subscription + description: | + If the user already has a premium access up to a certain time, this shortens/revokes his/her premium access. + If the ends is less than or equal to the current time, it revokes the subscription and sets the end date to be the current time, else it just shortens the subscription. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: subscription + in: body + required: true + schema: + type: object + required: + - ends + properties: + ends: + $ref: '#/definitions/datetimeString' + responses: + "200": + description: 'The affected user' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/license: + put: + tags: + - users + summary: Grant User License + operationId: users-grant-license + description: | + Grants a user access to the "Classroom" version up to a certain time. + Sets their role to "student". + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: license + in: body + required: true + schema: + type: object + required: + - ends + properties: + ends: + $ref: '#/definitions/datetimeString' + responses: + "200": + description: 'The affected user' + schema: + $ref: '#/definitions/UserResponse' + + /users/{handle}/shorten-license: + put: + summary: Shorten User License + tags: + - users + operationId: users-shorten-license + description: | + If the user already has access to the "Classroom" version up to a certain time, this shortens/revokes his/her access. + If the ends is less than or equal to the current time, it revokes the enrollment and sets the end date to be the current time, else it just shortens the enrollment. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: license + in: body + required: true + schema: + type: object + required: + - ends + properties: + ends: + $ref: '#/definitions/datetimeString' + responses: + "200": + description: 'The affected user' + schema: + $ref: '#/definitions/UserResponse' + + /clan/{handle}/members: + put: + tags: + - clans + summary: Upsert User Into Clan + operationId: clans-upsert-member + description: Upserts a user into the clan. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: member + in: body + required: true + schema: + type: object + required: + - userId + properties: + userId: + description: "The `_id` or `slug` of the user to add to the clan." + type: string + responses: + "200": + description: 'The clan with the member added.' + schema: + $ref: '#/definitions/ClanResponse' + + /classrooms: + post: + tags: + - classrooms + summary: Create a classroom + operationId: classrooms-create + description: Creates a new empty `Classroom`. + parameters: + - name: classroom + in: body + required: true + schema: + type: object + required: + - name + - ownerID + - aceConfig + properties: + name: + type: string + description: Name of the classroom + ownerID: + $ref: '#/definitions/objectIdString' + description: The `_id` of the teacher for whom classroom is being created + aceConfig: + type: object + properties: + language: + type: string + description: Programming language for the classroom + responses: + "201": + description: 'The created classroom' + schema: + $ref: '#/definitions/ClassroomResponseWithCode' + + get: + summary: Get Classroom Details + tags: + - classrooms + operationId: classrooms-get + description: Returns the classroom details for a class code. + parameters: + - name: code + in: query + type: string + required: true + description: The classroom's `code`. + - name: retMemberLimit + in: query + description: "limit the return number of members for the classroom" + required: false + type: number + responses: + "200": + description: 'The classroom details.' + schema: + $ref: '#/definitions/ClassroomResponseWithCode' + + /classrooms/{handle}/members: + put: + tags: + - classrooms + operationId: classrooms-upsert-member + summary: Upsert a user from classroom + description: Upserts a user into the classroom. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: member + in: body + required: true + schema: + type: object + required: + - code + - userId + properties: + code: + type: string + description: "The code for joining this classroom" + userId: + description: "The `_id` or `slug` of the user to add to the class." + type: string + retMemberLimit: + description: "limit the return number of members for the classroom, the default value is 1000" + type: number + responses: + "200": + description: 'The classroom with the member added.' + schema: + $ref: '#/definitions/ClassroomResponse' + delete: + summary: Delete User from Classroom + tags: + - classrooms + operationId: classrooms-remove-member + description: Remove a user from the classroom. + parameters: + - $ref: '#/parameters/handlePathParameter' + - name: member + in: body + required: true + schema: + type: object + required: + - userId + properties: + userId: + description: "The `_id` or `slug` of the user to remove from the class." + type: string + retMemberLimit: + description: "limit the return number of members for the classroom, the default value is 1000" + type: number + responses: + "200": + description: 'The classroom with the member removed.' + schema: + $ref: '#/definitions/ClassroomResponse' + + + /classrooms/{classroomHandle}/courses/{courseHandle}/enrolled: + put: + summary: Enroll User in a Course + tags: + - classrooms + operationId: classrooms-enroll-user-in-course + description: | + Enrolls a user in a course in a classroom. + If the course is paid, user must have an active license. + User must be a member of the classroom. + parameters: + - name: classroomHandle + in: path + type: string + required: true + description: The classroom's `_id`. + - name: courseHandle + in: path + type: string + required: true + description: The course's `_id`. + - name: userId + description: "The `_id` or `slug` of the user to add to the class." + in: body + required: true + schema: + type: object + required: + - userId + properties: + userId: + $ref: '#/definitions/objectIdString' + - name: retMemberLimit + in: query + description: "limit the return number of members for the classroom, the default value is 1000" + required: false + type: number + + responses: + "200": + description: 'The classroom with the user enrolled.' + schema: + $ref: '#/definitions/ClassroomResponse' + + /classrooms/{classroomHandle}/courses/{courseHandle}/remove-enrolled: + put: + summary: Remove User from a classroom + tags: + - classrooms + operationId: classrooms-remove-enrolled-user + description: | + Removes an enrolled user from a course in a classroom. + parameters: + - name: classroomHandle + in: path + type: string + required: true + description: The classroom's `_id`. + - name: courseHandle + in: path + type: string + required: true + description: The course's `_id`. + - name: userId + description: "The `_id` or `slug` of the user to remove from the course." + in: body + required: true + schema: + type: object + required: + - userId + properties: + userId: + $ref: '#/definitions/objectIdString' + - name: retMemberLimit + in: query + description: "limit the return number of members for the classroom, the default value is 1000" + required: false + type: number + responses: + "200": + description: 'The classroom with the user removed from the course.' + schema: + $ref: '#/definitions/ClassroomResponse' + + /classrooms/{classroomHandle}/stats: + get: + summary: Get Member Stats + tags: + - classrooms + operationId: classrooms-get-members-stats + description: | + Returns a list of all members stats for the classroom. + parameters: + - name: classroomHandle + in: path + type: string + required: true + description: The classroom's `_id`. + - name: project + in: query + type: string + description: | + If specified, include only the specified projection of returned stats; else, return all stats. Format as a comma-separated list, like `creator,playtime,state.complete`. + required: false + - name: memberLimit + in: query + type: number + description: 'Limit the return member number. the default value is 10, and the max value is 100' + required: false + - name: memberSkip + in: query + type: number + description: | + Skip the members that doesn't need to return, for pagination + required: false + + responses: + "200": + description: 'The members stats for the classroom.' + schema: + type: array + items: + type: object + properties: + _id: + $ref: '#/definitions/objectIdString' + stats: + type: object + properties: + gamesCompleted: + type: number + playtime: + type: number + description: "Total play time in seconds" + + /classrooms/{classroomHandle}/members/{memberHandle}/sessions: + get: + summary: Get Level Session + tags: + - classrooms + operationId: classrooms-get-levels-played + description: | + Returns a list of all levels played by the user for the classroom. + parameters: + - name: classroomHandle + in: path + type: string + required: true + description: The classroom's `_id`. + - name: memberHandle + in: path + type: string + required: true + description: The classroom member's `_id`. + responses: + "200": + description: 'The classroom with the user enrolled.' + schema: + type: array + items: + $ref: '#/definitions/LevelSessionResponse' + + /user-lookup/{property}/{value}: + get: + summary: Search for User + tags: + - users + operationId: users-lookup + description: Redirects to `/users/{handle}` given a unique, identifying property + parameters: + - name: property + in: path + type: string + required: true + description: The property to lookup by. May either be `"israel-id"` or `"name"`. + - name: value + in: path + type: string + required: true + description: The value to be looked up. + responses: + "301": + description: "Redirect to `User` resource at `/users/{handle}`" + + /playtime-stats: + get: + summary: Get Playtime Stats + tags: + - stats + operationId: stats-get-playtime-stats + description: Returns the playtime stats + parameters: + - name: startDate + in: query + description: "Earliest an included user was created" + required: false + type: string + - name: endDate + in: query + description: "Latest an included user was created" + required: false + type: string + - name: country + in: query + description: "Filter by country string" + required: false + type: string + + responses: + "200": + description: "Returns the playtime stats accross all owned users." + schema: + $ref: '#/definitions/PlaytimeStatsResponse' + + /license-stats: + get: + summary: Get License Stats + tags: + - stats + operationId: stats-get-license-stats + description: Returns the license stats + + responses: + "200": + description: "Returns the license stats for classroom/home subscription licenses." + schema: + $ref: '#/definitions/LicenseStatsResponse' + +parameters: + handlePathParameter: + name: handle + in: path + type: string + required: true + description: The document's `_id` or `slug`. + +tags: + - name: "users" + +securityDefinitions: + basicAuth: + type: basic + description: HTTP Basic Authentication. We will need to provide you with your client ID and secret. + +security: + - basicAuth: [] + +definitions: + roleString: + type: string + description: "Usually either 'teacher' or 'student'" + + datetimeString: + type: string + pattern: /^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}\.\d{3}Z$/ + + objectIdString: + type: string + pattern: /^[0-9a-f]{24}$/ + + UserResponse: + type: object + title: 'UserResponse' + description: 'Subset of properties listed here' + properties: + _id: + $ref: '#/definitions/objectIdString' + email: + type: string + name: + type: string + slug: + type: string + role: + $ref: '#/definitions/roleString' + stats: + type: object + properties: + gamesCompleted: + type: number + concepts: + type: object + additionalProperties: + type: number + playTime: + type: number + description: 'Included only when specifically requested on the endpoint' + oAuthIdentities: + type: array + items: + type: object + properties: + provider: + type: string + id: + type: string + subscription: + type: object + properties: + ends: + $ref: '#/definitions/datetimeString' + active: + type: 'boolean' + license: + type: object + properties: + ends: + $ref: '#/definitions/datetimeString' + active: + type: 'boolean' + + ClassroomResponse: + type: object + title: 'ClassroomResponse' + description: 'Subset of properties listed here' + properties: + _id: + $ref: '#/definitions/objectIdString' + name: + type: string + members: + type: array + items: + $ref: '#/definitions/objectIdString' + ownerID: + $ref: '#/definitions/objectIdString' + description: + type: string + courses: + type: array + items: + type: object + properties: + _id: + $ref: '#/definitions/objectIdString' + levels: + type: array + items: + type: object + enrolled: + type: array + items: + $ref: '#/definitions/objectIdString' + instance_id: + $ref: '#/definitions/objectIdString' + + ClassroomResponseWithCode: + type: object + title: 'ClassroomResponseWithCode' + description: 'Subset of properties listed here' + properties: + _id: + $ref: '#/definitions/objectIdString' + name: + type: string + members: + type: array + items: + $ref: '#/definitions/objectIdString' + ownerID: + $ref: '#/definitions/objectIdString' + description: + type: string + code: + type: string + codeCamel: + type: string + courses: + type: array + items: + type: object + properties: + _id: + $ref: '#/definitions/objectIdString' + levels: + type: array + items: + type: object + enrolled: + type: array + items: + $ref: '#/definitions/objectIdString' + instance_id: + $ref: '#/definitions/objectIdString' + clanId: + $ref: '#/definitions/objectIdString' + + PlaytimeStatsResponse: + type: object + properties: + playTime: + type: number + description: "Total play time in seconds" + gamesPlayed: + type: number + description: "Number of levels played" + + LicenseStatsResponse: + type: object + properties: + licenseDaysGranted: + type: number + description: "Total number of license days granted" + licenseDaysUsed: + type: number + description: "Number of license days used" + licenseDaysRemaining: + type: number + description: "Number of license days remaining" + activeLicenses: + type: number + description: "Number of active/valid licenses" + + LevelSessionResponse: + type: object + properties: + state: + type: object + properties: + complete: + type: boolean + level: + type: object + properties: + original: + type: string + description: 'The id for the level.' + levelID: + type: 'string' + description: 'Level slug like `wakka-maul`' + creator: + type: 'string' + $ref: '#/definitions/objectIdString' + playtime: + type: 'integer' + description: 'Time played in seconds.' + changed: + $ref: '#/definitions/datetimeString' + created: + $ref: '#/definitions/datetimeString' + dateFirstCompleted: + $ref: '#/definitions/datetimeString' + submitted: + type: 'boolean' + description: 'For arenas. Whether or not the level has been added to the ladder.' + published: + type: 'boolean' + description: 'For shareable projects. Whether or not the project has been shared with classmates.' + + ClanResponse: + type: object + title: 'ClanResponse' + description: 'Subset of properties listed here' + properties: + _id: + $ref: '#/definitions/objectIdString' + name: + type: string + displayName: + type: string + members: + type: array + items: + $ref: '#/definitions/objectIdString' + ownerID: + $ref: '#/definitions/objectIdString' + description: + type: string + type: + type: string + kind: + type: string + metadata: + type: object \ No newline at end of file diff --git a/fern/assets/favicon.png b/fern/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..2eff160701f58dc71df81e3733d274627b6c1d68 GIT binary patch literal 14289 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 literal 0 HcmV?d00001 diff --git a/fern/assets/logo.png b/fern/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a2789611a89c8893bce51b1c0d7cb302347eba49 GIT binary patch literal 31256 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�Z(#Skti-1lT8>bV4|Z4AAajoOrDwJ$DRe}E!W>p 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 console.log(res.statusCode, res.body)) +``` + +## 用户授权 +通过您的服务在CodeCombat平台上认证一个用户,您需要采用以下oAuth 2流程. CodeCombat作为一个客户,您的服务作为服务提供者。首先,您需要提供一个可信的查找网址(lookup URL 或者 token URL),来进行创建设置(参见以上客户创建说明),这个创建账户和登陆的过程如以下所示: + +1. **创建用户** +2. **把CodeCombat用户和一个oAuth身份对接** + 你可以使用代码或者准入指令(access token)来启动这个API嵌入。如果你没有获得准入指令(access token),我们会用这个URL指令来替换原有的代码来获得一个准入指令(access token)。 + 然后我们用准入指令(access token)来启动查找网址(lookup URL),以此从你的系统中获得用户信息(`id`),这些信息已被储存在我们的用户数据库中。 +3. **用户登陆** + 你可以用这个代码/准入指令(access token)来call API, 然后我们会通过类似于第二步中的步骤来获得用户信息。 + 最后,我们将数据库在第二步中获得的信息与这个信息进行匹配,如果匹配成功,用户即可完成登录并被引导到主页。 + +这里还有一个描述以上过程的 [实例](https://s3.amazonaws.com/files.codecombat.com/codecombat_oauth_example.tar.gz),以便理解。 同时,你也可以参考这个 [图表](https://s3.amazonaws.com/files.codecombat.com/Example_OAuth_Flow.png)。 \ No newline at end of file diff --git a/fern/intro-en.mdx b/fern/intro-en.mdx new file mode 100644 index 0000000..b8b30df --- /dev/null +++ b/fern/intro-en.mdx @@ -0,0 +1,58 @@ +## Basics + +- Examples are in JavaScript on a Node/Express server with + [request](https://github.com/request/request) installed. +- Request and responses are in JSON +- API responses are the base resource being created/referenced. So, for example, + all routes starting with `/api/users` return [User](/users/post-users) + resources. + +## Client Setup + +We currently do not have a way for you to create or set up your own API Client +or OAuth Provider information. Please contact us directly to get started. + +## Client Authentication + +API routes must be called with Basic HTTP Authentication. You will receive a +username (CLIENT_ID) and password (CLIENT_SECRET) upon creation of your API +Client in our system. Provide those credentials with each API request. + +```javascript +url = "https://codecombat.com/api/users"; +json = { name: "A username" }; +auth = { name: CLIENT_ID, pass: CLIENT_SECRET }; +request.get({ url, json, auth }, (err, res) => + console.log(res.statusCode, res.body) +); +``` + +We strongly recommend using a secrets manager for storing your client secret. +Plain text files like dotenv lead to accidental, costly leaks. Use +[Doppler](https://www.doppler.com/) for a developer-friendly experience. AWS and +Google Cloud have native solutions as well. + +## User Authentication + +To authenticate a user on CodeCombat through your service, you will need to use +the below OAuth 2 process. CodeCombat will act as the client, and your service +will act as the provider. First, you will need to provide a trusted lookup URL +and/or a token URL for the setup(See Client Setup above). Then the process from +user account creation to log in will look like this: + +1. **Create the user** +2. **Link the CodeCombat user to an OAuth identity** + You can call this API with a code or an access token. If no access token is + given, we will use the token URL to exchange the given code for an access + token. Then we call the lookup URL with the access token to receive the user + information (id) from your system which is saved to the user in our db. +3. **Log the user in** + You can call this API with the code/access token, and we will get the user + information from your system similarly to step 2. Finally, we match this + information with what is stored in our database in step 2. If everything + checks out, the user is logged in and redirected to the home page. + +There is also a +[concrete example](https://s3.amazonaws.com/files.codecombat.com/codecombat_oauth_example.tar.gz) +depicting the above process for better understanding. You can also refer to this +[diagram](https://s3.amazonaws.com/files.codecombat.com/Example_OAuth_Flow.png). From 05a57793889d18a53d68333ba4fd1b6ea2c6c599 Mon Sep 17 00:00:00 2001 From: dsinghvi Date: Wed, 23 Aug 2023 20:24:02 -0400 Subject: [PATCH 2/2] update url --- fern/apis/chinese/openapi/openapi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/apis/chinese/openapi/openapi.yml b/fern/apis/chinese/openapi/openapi.yml index 0f02ef8..4fa7965 100644 --- a/fern/apis/chinese/openapi/openapi.yml +++ b/fern/apis/chinese/openapi/openapi.yml @@ -54,7 +54,7 @@ paths: description: | 用 [user](#users) 登陆. - 在这个示例中,我们用准入指令(access token)(`1234`)来call你的查找的URL(lookup URL)(比方说,是 ‘https://oauth.provider/user?t=<%=accessToken%>` 。在这个例子中,返回的查找URL(lookup URL)是 `{ id: 'abcd' }`。我们将数据库中储存的OAuthIdentity用户信息与这个`id`匹配。如果匹配成功,用户即可登录并被引导到主页。 + 在这个示例中,我们用准入指令(access token)(`1234`)来call你的查找的URL(lookup URL)(比方说,是 `https://oauth.provider/user?t=<%=accessToken%>` 。在这个例子中,返回的查找URL(lookup URL)是 `{ id: 'abcd' }`。我们将数据库中储存的OAuthIdentity用户信息与这个`id`匹配。如果匹配成功,用户即可登录并被引导到主页。 parameters : - name: provider