mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-11-03 20:45:58 +01:00 
			
		
		
		
	feat: added DELETE /api/v1/users/:uid and DELETE /api/v1/users
This commit is contained in:
		
							
								
								
									
										348
									
								
								openapi.yaml
									
									
									
									
									
								
							
							
						
						
									
										348
									
								
								openapi.yaml
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
openapi: 3.0.0
 | 
			
		||||
info:
 | 
			
		||||
  description: Standard, out-of-the-box read & write API for NodeBB v2.0+
 | 
			
		||||
  version: "1.0.0"
 | 
			
		||||
  description: 'Standard, out-of-the-box read & write API for NodeBB v2.0+'
 | 
			
		||||
  version: 31-03-2020
 | 
			
		||||
  title: Read/Write API
 | 
			
		||||
  contact:
 | 
			
		||||
    email: support@nodebb.org
 | 
			
		||||
@@ -9,80 +9,14 @@ info:
 | 
			
		||||
    name: MIT
 | 
			
		||||
    url: 'https://opensource.org/licenses/MIT'
 | 
			
		||||
tags:
 | 
			
		||||
  # - name: admins
 | 
			
		||||
  #   description: Secured Admin-only calls
 | 
			
		||||
  # - name: developers
 | 
			
		||||
  #   description: Operations available to regular developers
 | 
			
		||||
  - name: users
 | 
			
		||||
    description: Account related calls (create, modify, delete, etc.)
 | 
			
		||||
    description: 'Account related calls (create, modify, delete, etc.)'
 | 
			
		||||
paths:
 | 
			
		||||
  # /inventory:
 | 
			
		||||
  #   get:
 | 
			
		||||
  #     tags:
 | 
			
		||||
  #       - developers
 | 
			
		||||
  #     summary: searches inventory
 | 
			
		||||
  #     operationId: searchInventory
 | 
			
		||||
  #     description: |
 | 
			
		||||
  #       By passing in the appropriate options, you can search for
 | 
			
		||||
  #       available inventory in the system
 | 
			
		||||
  #     parameters:
 | 
			
		||||
  #       - in: query
 | 
			
		||||
  #         name: searchString
 | 
			
		||||
  #         description: pass an optional search string for looking up inventory
 | 
			
		||||
  #         required: false
 | 
			
		||||
  #         schema:
 | 
			
		||||
  #           type: string
 | 
			
		||||
  #       - in: query
 | 
			
		||||
  #         name: skip
 | 
			
		||||
  #         description: number of records to skip for pagination
 | 
			
		||||
  #         schema:
 | 
			
		||||
  #           type: integer
 | 
			
		||||
  #           format: int32
 | 
			
		||||
  #           minimum: 0
 | 
			
		||||
  #       - in: query
 | 
			
		||||
  #         name: limit
 | 
			
		||||
  #         description: maximum number of records to return
 | 
			
		||||
  #         schema:
 | 
			
		||||
  #           type: integer
 | 
			
		||||
  #           format: int32
 | 
			
		||||
  #           minimum: 0
 | 
			
		||||
  #           maximum: 50
 | 
			
		||||
  #     responses:
 | 
			
		||||
  #       '200':
 | 
			
		||||
  #         description: search results matching criteria
 | 
			
		||||
  #         content:
 | 
			
		||||
  #           application/json:
 | 
			
		||||
  #             schema:
 | 
			
		||||
  #               type: array
 | 
			
		||||
  #               items:
 | 
			
		||||
  #                 $ref: '#/components/schemas/InventoryItem'
 | 
			
		||||
  #       '400':
 | 
			
		||||
  #         description: bad input parameter
 | 
			
		||||
  #   post:
 | 
			
		||||
  #     tags:
 | 
			
		||||
  #       - admins
 | 
			
		||||
  #     summary: adds an inventory item
 | 
			
		||||
  #     operationId: addInventory
 | 
			
		||||
  #     description: Adds an item to the system
 | 
			
		||||
  #     responses:
 | 
			
		||||
  #       '201':
 | 
			
		||||
  #         description: item created
 | 
			
		||||
  #       '400':
 | 
			
		||||
  #         description: 'invalid input, object invalid'
 | 
			
		||||
  #       '409':
 | 
			
		||||
  #         description: an existing item already exists
 | 
			
		||||
  #     requestBody:
 | 
			
		||||
  #       content:
 | 
			
		||||
  #         application/json:
 | 
			
		||||
  #           schema:
 | 
			
		||||
  #             $ref: '#/components/schemas/InventoryItem'
 | 
			
		||||
  #       description: Inventory item to add
 | 
			
		||||
  /:
 | 
			
		||||
    post:
 | 
			
		||||
      tags:
 | 
			
		||||
        - users
 | 
			
		||||
      summary: creates a user account
 | 
			
		||||
      operationId: createUser
 | 
			
		||||
      description: This operation creates a new user account
 | 
			
		||||
      requestBody:
 | 
			
		||||
        required: true
 | 
			
		||||
@@ -93,7 +27,7 @@ paths:
 | 
			
		||||
              properties:
 | 
			
		||||
                username:
 | 
			
		||||
                  type: string
 | 
			
		||||
                  description: If the username is taken, a number will be appended
 | 
			
		||||
                  description: 'If the username is taken, a number will be appended'
 | 
			
		||||
                password:
 | 
			
		||||
                  type: string
 | 
			
		||||
                email:
 | 
			
		||||
@@ -110,46 +44,144 @@ paths:
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/User'
 | 
			
		||||
                type: object
 | 
			
		||||
                properties:
 | 
			
		||||
                  status:
 | 
			
		||||
                    $ref: '#/components/schemas/Status'
 | 
			
		||||
                  response:
 | 
			
		||||
                    $ref: '#/components/schemas/UserObj'
 | 
			
		||||
        '400':
 | 
			
		||||
          $ref: '#/components/responses/400'
 | 
			
		||||
        '401':
 | 
			
		||||
          $ref: '#/components/responses/401'
 | 
			
		||||
        '403':
 | 
			
		||||
          $ref: '#/components/responses/403'
 | 
			
		||||
        '426':
 | 
			
		||||
          $ref: '#/components/responses/426'
 | 
			
		||||
        '500':
 | 
			
		||||
          $ref: '#/components/responses/500'
 | 
			
		||||
    delete:
 | 
			
		||||
      tags:
 | 
			
		||||
        - users
 | 
			
		||||
      summary: deletes one or more users
 | 
			
		||||
      description: This operation deletes one or many user accounts, including their contributions (posts, topics, etc.)
 | 
			
		||||
      requestBody:
 | 
			
		||||
        required: true
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              type: object
 | 
			
		||||
              properties:
 | 
			
		||||
                uids:
 | 
			
		||||
                  type: array
 | 
			
		||||
                  description: A collection of uids
 | 
			
		||||
            example:
 | 
			
		||||
              uids:
 | 
			
		||||
                - 1
 | 
			
		||||
                - 2
 | 
			
		||||
                - 3
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: user account(s) deleted
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: object
 | 
			
		||||
                properties:
 | 
			
		||||
                  status:
 | 
			
		||||
                    $ref: '#/components/schemas/Status'
 | 
			
		||||
                  response:
 | 
			
		||||
                    type: object
 | 
			
		||||
  '/{uid}':
 | 
			
		||||
    put:
 | 
			
		||||
      tags:
 | 
			
		||||
        - users
 | 
			
		||||
      summary: updates a user account
 | 
			
		||||
      parameters:
 | 
			
		||||
        - in: path
 | 
			
		||||
          name: uid
 | 
			
		||||
          schema:
 | 
			
		||||
            type: integer
 | 
			
		||||
          required: true
 | 
			
		||||
          description: uid of the user to update
 | 
			
		||||
      requestBody:
 | 
			
		||||
        required: true
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: '#/components/schemas/UserRequest'
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: user profile updated
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: object
 | 
			
		||||
                properties:
 | 
			
		||||
                  status:
 | 
			
		||||
                    $ref: '#/components/schemas/Status'
 | 
			
		||||
                  response:
 | 
			
		||||
                    $ref: '#/components/schemas/UserObj'
 | 
			
		||||
        '401':
 | 
			
		||||
          $ref: '#/components/responses/401'
 | 
			
		||||
        '403':
 | 
			
		||||
          $ref: '#/components/responses/403'
 | 
			
		||||
        '426':
 | 
			
		||||
          $ref: '#/components/responses/426'
 | 
			
		||||
        '500':
 | 
			
		||||
          $ref: '#/components/responses/500'
 | 
			
		||||
    delete:
 | 
			
		||||
      tags:
 | 
			
		||||
        - users
 | 
			
		||||
      summary: delete a single user account
 | 
			
		||||
      parameters:
 | 
			
		||||
        - in: path
 | 
			
		||||
          name: uid
 | 
			
		||||
          schema:
 | 
			
		||||
            type: integer
 | 
			
		||||
          required: true
 | 
			
		||||
          description: uid of the user to delete
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: user account deleted
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: object
 | 
			
		||||
                properties:
 | 
			
		||||
                  status:
 | 
			
		||||
                    $ref: '#/components/schemas/Status'
 | 
			
		||||
                  response:
 | 
			
		||||
                    type: object
 | 
			
		||||
components:
 | 
			
		||||
  schemas:
 | 
			
		||||
    # InventoryItem:
 | 
			
		||||
    #   type: object
 | 
			
		||||
    #   required:
 | 
			
		||||
    #     - id
 | 
			
		||||
    #     - name
 | 
			
		||||
    #     - manufacturer
 | 
			
		||||
    #     - releaseDate
 | 
			
		||||
    #   properties:
 | 
			
		||||
    #     id:
 | 
			
		||||
    #       type: string
 | 
			
		||||
    #       format: uuid
 | 
			
		||||
    #       example: d290f1ee-6c54-4b01-90e6-d701748f0851
 | 
			
		||||
    #     name:
 | 
			
		||||
    #       type: string
 | 
			
		||||
    #       example: Widget Adapter
 | 
			
		||||
    #     releaseDate:
 | 
			
		||||
    #       type: string
 | 
			
		||||
    #       format: date-time
 | 
			
		||||
    #       example: '2016-08-29T09:12:33.001Z'
 | 
			
		||||
    #     manufacturer:
 | 
			
		||||
    #       $ref: '#/components/schemas/Manufacturer'
 | 
			
		||||
    # Manufacturer:
 | 
			
		||||
    #   required:
 | 
			
		||||
    #     - name
 | 
			
		||||
    #   properties:
 | 
			
		||||
    #     name:
 | 
			
		||||
    #       type: string
 | 
			
		||||
    #       example: ACME Corporation
 | 
			
		||||
    #     homePage:
 | 
			
		||||
    #       type: string
 | 
			
		||||
    #       format: url
 | 
			
		||||
    #       example: 'https://www.acme-corp.com'
 | 
			
		||||
    #     phone:
 | 
			
		||||
    #       type: string
 | 
			
		||||
    #       example: 408-867-5309
 | 
			
		||||
    #   type: object
 | 
			
		||||
    User:
 | 
			
		||||
    Status:
 | 
			
		||||
      type: object
 | 
			
		||||
      properties:
 | 
			
		||||
        code:
 | 
			
		||||
          allOf:
 | 
			
		||||
            - title: Success
 | 
			
		||||
              type: string
 | 
			
		||||
              example: ok
 | 
			
		||||
            - title: Error
 | 
			
		||||
              type: string
 | 
			
		||||
              example: error
 | 
			
		||||
        message:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: OK
 | 
			
		||||
    Error:
 | 
			
		||||
      type: object
 | 
			
		||||
      properties:
 | 
			
		||||
        status:
 | 
			
		||||
          type: object
 | 
			
		||||
          properties:
 | 
			
		||||
            code:
 | 
			
		||||
              type: string
 | 
			
		||||
            message:
 | 
			
		||||
              type: string
 | 
			
		||||
        response:
 | 
			
		||||
          type: object
 | 
			
		||||
    UserObj:
 | 
			
		||||
      properties:
 | 
			
		||||
        uid:
 | 
			
		||||
          type: number
 | 
			
		||||
@@ -163,7 +195,7 @@ components:
 | 
			
		||||
        email:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: dragonfruit@example.org
 | 
			
		||||
        email:confirmed:
 | 
			
		||||
        'email:confirmed':
 | 
			
		||||
          type: number
 | 
			
		||||
          example: 1
 | 
			
		||||
        joindate:
 | 
			
		||||
@@ -174,20 +206,20 @@ components:
 | 
			
		||||
          example: 1585337827953
 | 
			
		||||
        picture:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80
 | 
			
		||||
          example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
 | 
			
		||||
        fullname:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: Mr. Dragon Fruit Jr.
 | 
			
		||||
        location:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: Toronto, Canada
 | 
			
		||||
          example: 'Toronto, Canada'
 | 
			
		||||
        birthday:
 | 
			
		||||
          type: string
 | 
			
		||||
          description: A birthdate given in an ISO format parseable by the Date object
 | 
			
		||||
          example: 03/27/2020
 | 
			
		||||
        website:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: https://example.org
 | 
			
		||||
          example: 'https://example.org'
 | 
			
		||||
        aboutme:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: |
 | 
			
		||||
@@ -202,7 +234,7 @@ components:
 | 
			
		||||
        uploadedpicture:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: /assets/profile/1-profileimg.png
 | 
			
		||||
          description: In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum.
 | 
			
		||||
          description: 'In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum.'
 | 
			
		||||
        profileviews:
 | 
			
		||||
          type: number
 | 
			
		||||
          example: 1000
 | 
			
		||||
@@ -221,7 +253,7 @@ components:
 | 
			
		||||
        banned:
 | 
			
		||||
          type: number
 | 
			
		||||
          example: 0
 | 
			
		||||
        banned:expire:
 | 
			
		||||
        'banned:expire':
 | 
			
		||||
          type: number
 | 
			
		||||
          example: 1585337827953
 | 
			
		||||
        status:
 | 
			
		||||
@@ -236,35 +268,107 @@ components:
 | 
			
		||||
        followingcount:
 | 
			
		||||
          type: number
 | 
			
		||||
          example: 5
 | 
			
		||||
        cover:url:
 | 
			
		||||
        'cover:url':
 | 
			
		||||
          type: string
 | 
			
		||||
          example: /assets/profile/1-cover.png
 | 
			
		||||
        cover:position:
 | 
			
		||||
        'cover:position':
 | 
			
		||||
          type: string
 | 
			
		||||
          example: 50.0301% 19.2464%
 | 
			
		||||
        groupTitle:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: "[\"administrators\",\"Staff\"]"
 | 
			
		||||
          example: '["administrators","Staff"]'
 | 
			
		||||
        groupTitleArray:
 | 
			
		||||
          type: array
 | 
			
		||||
          example:
 | 
			
		||||
            - administrators
 | 
			
		||||
            - Staff
 | 
			
		||||
        icon:text:
 | 
			
		||||
        'icon:text':
 | 
			
		||||
          type: string
 | 
			
		||||
          example: D
 | 
			
		||||
        icon:bgColor:
 | 
			
		||||
        'icon:bgColor':
 | 
			
		||||
          type: string
 | 
			
		||||
          example: "#9c27b0"
 | 
			
		||||
          example: '#9c27b0'
 | 
			
		||||
        joindateISO:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: 2020-03-27T20:30:36.590Z
 | 
			
		||||
          example: '2020-03-27T20:30:36.590Z'
 | 
			
		||||
        lastonlineISO:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: 2020-03-27T20:30:36.590Z
 | 
			
		||||
          example: '2020-03-27T20:30:36.590Z'
 | 
			
		||||
        banned_until:
 | 
			
		||||
          type: number
 | 
			
		||||
          example: 0
 | 
			
		||||
        banned_until_readable:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: Not Banned
 | 
			
		||||
    UserRequest:
 | 
			
		||||
      properties:
 | 
			
		||||
        username:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: Dragon Fruit
 | 
			
		||||
        email:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: dragonfruit@example.org
 | 
			
		||||
        fullname:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: Mr. Dragon Fruit Jr.
 | 
			
		||||
        website:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: 'https://example.org'
 | 
			
		||||
        location:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: 'Toronto, Canada'
 | 
			
		||||
        groupTitle:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: '["administrators","Staff"]'
 | 
			
		||||
        birthday:
 | 
			
		||||
          type: string
 | 
			
		||||
          description: A birthdate given in an ISO format parseable by the Date object
 | 
			
		||||
          example: 03/27/2020
 | 
			
		||||
        signature:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: |
 | 
			
		||||
            This is an example signature
 | 
			
		||||
            It can span multiple lines.
 | 
			
		||||
        aboutme:
 | 
			
		||||
          type: string
 | 
			
		||||
          example: |
 | 
			
		||||
            This is a paragraph all about how my life got twist-turned upside-down
 | 
			
		||||
            and I'd like to take a minute and sit right here,
 | 
			
		||||
            to tell you all about how I because the administrator of NodeBB
 | 
			
		||||
  responses:
 | 
			
		||||
    '400':
 | 
			
		||||
      description: Bad Request
 | 
			
		||||
      content:
 | 
			
		||||
        application/json:
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/components/schemas/Error'
 | 
			
		||||
    '401':
 | 
			
		||||
      description: Not Authorized
 | 
			
		||||
      content:
 | 
			
		||||
        application/json:
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/components/schemas/Error'
 | 
			
		||||
    '403':
 | 
			
		||||
      description: Forbidden
 | 
			
		||||
      content:
 | 
			
		||||
        application/json:
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/components/schemas/Error'
 | 
			
		||||
    '404':
 | 
			
		||||
      description: Not Found
 | 
			
		||||
      content:
 | 
			
		||||
        application/json:
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/components/schemas/Error'
 | 
			
		||||
    '426':
 | 
			
		||||
      description: Upgrade Required
 | 
			
		||||
      content:
 | 
			
		||||
        application/json:
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/components/schemas/Error'
 | 
			
		||||
    '500':
 | 
			
		||||
      description: Internal Server Error
 | 
			
		||||
      content:
 | 
			
		||||
        application/json:
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/components/schemas/Error'
 | 
			
		||||
 
 | 
			
		||||
@@ -42,18 +42,20 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
 | 
			
		||||
 | 
			
		||||
		$(window).trigger('action:profile.update', userData);
 | 
			
		||||
 | 
			
		||||
		socket.emit('user.updateProfile', userData, function (err, data) {
 | 
			
		||||
			if (err) {
 | 
			
		||||
				return app.alertError(err.message);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		$.ajax({
 | 
			
		||||
			url: config.relative_path + '/api/v1/users/' + userData.uid,
 | 
			
		||||
			data: userData,
 | 
			
		||||
			method: 'put',
 | 
			
		||||
		}).done(function (res) {
 | 
			
		||||
			app.alertSuccess('[[user:profile_update_success]]');
 | 
			
		||||
 | 
			
		||||
			if (data.picture) {
 | 
			
		||||
				$('#user-current-picture').attr('src', data.picture);
 | 
			
		||||
			if (res.response.picture) {
 | 
			
		||||
				$('#user-current-picture').attr('src', res.response.picture);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			updateHeader(data.picture);
 | 
			
		||||
			updateHeader(res.response.picture);
 | 
			
		||||
		}).fail(function (ev) {
 | 
			
		||||
			return app.alertError(ev.responseJSON.status.message);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -27,13 +27,15 @@ define('forum/account/edit/email', ['forum/account/header'], function (header) {
 | 
			
		||||
			var btn = $(this);
 | 
			
		||||
			btn.addClass('disabled').find('i').removeClass('hide');
 | 
			
		||||
 | 
			
		||||
			socket.emit('user.changeUsernameEmail', userData, function (err) {
 | 
			
		||||
			$.ajax({
 | 
			
		||||
				url: config.relative_path + '/api/v1/users/' + userData.uid,
 | 
			
		||||
				data: userData,
 | 
			
		||||
				method: 'put',
 | 
			
		||||
			}).done(function (res) {
 | 
			
		||||
				btn.removeClass('disabled').find('i').addClass('hide');
 | 
			
		||||
				if (err) {
 | 
			
		||||
					return app.alertError(err.message);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				ajaxify.go('user/' + ajaxify.data.userslug + '/edit');
 | 
			
		||||
				ajaxify.go('user/' + res.response.userslug + '/edit');
 | 
			
		||||
			}).fail(function (ev) {
 | 
			
		||||
				app.alertError(ev.responseJSON.status.message);
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -24,22 +24,25 @@ define('forum/account/edit/username', ['forum/account/header'], function (header
 | 
			
		||||
 | 
			
		||||
			var btn = $(this);
 | 
			
		||||
			btn.addClass('disabled').find('i').removeClass('hide');
 | 
			
		||||
			socket.emit('user.changeUsernameEmail', userData, function (err, data) {
 | 
			
		||||
				btn.removeClass('disabled').find('i').addClass('hide');
 | 
			
		||||
				if (err) {
 | 
			
		||||
					return app.alertError(err.message);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			$.ajax({
 | 
			
		||||
				url: config.relative_path + '/api/v1/users/' + userData.uid,
 | 
			
		||||
				data: userData,
 | 
			
		||||
				method: 'put',
 | 
			
		||||
			}).done(function (res) {
 | 
			
		||||
				btn.removeClass('disabled').find('i').addClass('hide');
 | 
			
		||||
				var userslug = utils.slugify(userData.username);
 | 
			
		||||
				if (userData.username && userslug && parseInt(userData.uid, 10) === parseInt(app.user.uid, 10)) {
 | 
			
		||||
					$('[component="header/profilelink"]').attr('href', config.relative_path + '/user/' + userslug);
 | 
			
		||||
					$('[component="header/profilelink/edit"]').attr('href', config.relative_path + '/user/' + userslug + '/edit');
 | 
			
		||||
					$('[component="header/profilelink/settings"]').attr('href', config.relative_path + '/user/' + userslug + '/settings');
 | 
			
		||||
					$('[component="header/username"]').text(userData.username);
 | 
			
		||||
					$('[component="header/usericon"]').css('background-color', data['icon:bgColor']).text(data['icon:text']);
 | 
			
		||||
					$('[component="header/usericon"]').css('background-color', res.response['icon:bgColor']).text(res.response['icon:text']);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				ajaxify.go('user/' + userslug + '/edit');
 | 
			
		||||
			}).fail(function (ev) {
 | 
			
		||||
				app.alertError(ev.responseJSON.status.message);
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -349,12 +349,16 @@ helpers.formatApiResponse = async (statusCode, res, payload) => {
 | 
			
		||||
			response: payload || {},
 | 
			
		||||
		});
 | 
			
		||||
	} else if (payload instanceof Error) {
 | 
			
		||||
		let message = '';
 | 
			
		||||
		if (isLanguageKey.test(payload.message)) {
 | 
			
		||||
			const translated = await translator.translate(payload.message, 'en-GB');
 | 
			
		||||
			res.status(statusCode).json(helpers.generateError(statusCode, translated));
 | 
			
		||||
			message = await translator.translate(payload.message, 'en-GB');
 | 
			
		||||
		} else {
 | 
			
		||||
			res.status(statusCode).json(helpers.generateError(statusCode, payload.message));
 | 
			
		||||
			message = payload.message;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const returnPayload = helpers.generateError(statusCode, message);
 | 
			
		||||
		returnPayload.stack = payload.stack;
 | 
			
		||||
		res.status(statusCode).json(returnPayload);
 | 
			
		||||
	} else if (!payload) {
 | 
			
		||||
		// Non-2xx statusCode, generate predefined error
 | 
			
		||||
		res.status(statusCode).json(helpers.generateError(statusCode));
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,108 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const users = require('../../user');
 | 
			
		||||
const user = require('../../user');
 | 
			
		||||
const groups = require('../../groups');
 | 
			
		||||
const privileges = require('../../privileges');
 | 
			
		||||
const meta = require('../../meta');
 | 
			
		||||
const events = require('../../events');
 | 
			
		||||
const helpers = require('../helpers');
 | 
			
		||||
 | 
			
		||||
const Users = module.exports;
 | 
			
		||||
 | 
			
		||||
Users.create = async (req, res) => {
 | 
			
		||||
	const uid = await users.create(req.body);
 | 
			
		||||
	helpers.formatApiResponse(200, res, await users.getUserData(uid));
 | 
			
		||||
	const uid = await user.create(req.body);
 | 
			
		||||
	helpers.formatApiResponse(200, res, await user.getUserData(uid));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Users.update = async (req, res) => {
 | 
			
		||||
	const oldUserData = await user.getUserFields(req.params.uid, ['email', 'username']);
 | 
			
		||||
	if (!oldUserData || !oldUserData.username) {
 | 
			
		||||
		throw new Error('[[error:invalid-data]]');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const [isAdminOrGlobalMod, canEdit, passwordMatch] = await Promise.all([
 | 
			
		||||
		user.isAdminOrGlobalMod(req.user.uid),
 | 
			
		||||
		privileges.users.canEdit(req.user.uid, req.params.uid),
 | 
			
		||||
		user.isPasswordCorrect(req.body.uid, req.body.password, req.ip),
 | 
			
		||||
	]);
 | 
			
		||||
 | 
			
		||||
	// Changing own email/username requires password confirmation
 | 
			
		||||
	if (req.user.uid === req.body.uid && !passwordMatch) {
 | 
			
		||||
		helpers.formatApiResponse(403, res, new Error('[[error:invalid-password]]'));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!canEdit) {
 | 
			
		||||
		helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]'));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!isAdminOrGlobalMod && meta.config['username:disableEdit']) {
 | 
			
		||||
		req.body.username = oldUserData.username;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!isAdminOrGlobalMod && meta.config['email:disableEdit']) {
 | 
			
		||||
		req.body.email = oldUserData.email;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.body.uid = req.params.uid;	// The `uid` argument in `updateProfile` refers to calling user, not target user
 | 
			
		||||
	await user.updateProfile(req.user.uid, req.body);
 | 
			
		||||
	const userData = await user.getUserData(req.body.uid);
 | 
			
		||||
 | 
			
		||||
	async function log(type, eventData) {
 | 
			
		||||
		eventData.type = type;
 | 
			
		||||
		eventData.uid = req.user.uid;
 | 
			
		||||
		eventData.targetUid = req.params.uid;
 | 
			
		||||
		eventData.ip = req.ip;
 | 
			
		||||
		await events.log(eventData);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (userData.email !== oldUserData.email) {
 | 
			
		||||
		await log('email-change', { oldEmail: oldUserData.email, newEmail: userData.email });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (userData.username !== oldUserData.username) {
 | 
			
		||||
		await log('username-change', { oldUsername: oldUserData.username, newUsername: userData.username });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	helpers.formatApiResponse(200, res, userData);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Users.delete = async (req, res) => {
 | 
			
		||||
	processDeletion(req.params.uid, req, res);
 | 
			
		||||
	helpers.formatApiResponse(200, res);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Users.deleteMany = async (req, res) => {
 | 
			
		||||
	await canDeleteUids(req.body.uids, res);
 | 
			
		||||
	await Promise.all(req.body.uids.map(uid => processDeletion(uid, req, res)));
 | 
			
		||||
	helpers.formatApiResponse(200, res);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function canDeleteUids(uids, res) {
 | 
			
		||||
	if (!Array.isArray(uids)) {
 | 
			
		||||
		helpers.formatApiResponse(400, res, new Error('[[error:invalid-data]]'));
 | 
			
		||||
	}
 | 
			
		||||
	const isMembers = await groups.isMembers(uids, 'administrators');
 | 
			
		||||
	if (isMembers.includes(true)) {
 | 
			
		||||
		helpers.formatApiResponse(403, res, new Error('[[error:cant-delete-other-admins]]'));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function processDeletion(uid, req, res) {
 | 
			
		||||
	const isTargetAdmin = await user.isAdministrator(uid);
 | 
			
		||||
	if (!res.locals.privileges.isSelf && !res.locals.privileges.isAdmin) {
 | 
			
		||||
		return helpers.formatApiResponse(403, res);
 | 
			
		||||
	} else if (!res.locals.privileges.isSelf && isTargetAdmin) {
 | 
			
		||||
		return helpers.formatApiResponse(403, res, new Error('[[error:cant-delete-other-admins]]'));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: clear user tokens for this uid
 | 
			
		||||
	const userData = await user.delete(req.user.uid, uid);
 | 
			
		||||
	await events.log({
 | 
			
		||||
		type: 'user-delete',
 | 
			
		||||
		uid: req.user.uid,
 | 
			
		||||
		targetUid: uid,
 | 
			
		||||
		ip: req.ip,
 | 
			
		||||
		username: userData.username,
 | 
			
		||||
		email: userData.email,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								src/middleware/expose.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/middleware/expose.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The middlewares here strictly act to "expose" certain values from the database,
 | 
			
		||||
 * into `res.locals` for use in middlewares and/or controllers down the line
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const user = require('../user');
 | 
			
		||||
const utils = require('../utils');
 | 
			
		||||
 | 
			
		||||
module.exports = function (middleware) {
 | 
			
		||||
	middleware.exposeAdmin = async (req, res, next) => {
 | 
			
		||||
		// Unlike `requireAdmin`, this middleware just checks the uid, and sets `isAdmin` in `res.locals`
 | 
			
		||||
		res.locals.isAdmin = false;
 | 
			
		||||
 | 
			
		||||
		if (!req.user) {
 | 
			
		||||
			return next();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const isAdmin = await user.isAdministrator(req.user.uid);
 | 
			
		||||
		res.locals.isAdmin = isAdmin;
 | 
			
		||||
		return next();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	middleware.exposePrivileges = async (req, res, next) => {
 | 
			
		||||
		// Exposes a hash of user's ranks (admin, gmod, etc.)
 | 
			
		||||
		const hash = await utils.promiseParallel({
 | 
			
		||||
			isAdmin: user.isAdministrator(req.user.uid),
 | 
			
		||||
			isGmod: user.isGlobalModerator(req.user.uid),
 | 
			
		||||
			isPrivileged: user.isPrivileged(req.user.uid),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (req.params.uid) {
 | 
			
		||||
			hash.isSelf = parseInt(req.params.uid, 10) === req.user.uid;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res.locals.privileges = hash;
 | 
			
		||||
		return next();
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
@@ -59,6 +59,7 @@ require('./render')(middleware);
 | 
			
		||||
require('./maintenance')(middleware);
 | 
			
		||||
require('./user')(middleware);
 | 
			
		||||
require('./headers')(middleware);
 | 
			
		||||
require('./expose')(middleware);
 | 
			
		||||
 | 
			
		||||
middleware.stripLeadingSlashes = function stripLeadingSlashes(req, res, next) {
 | 
			
		||||
	var target = req.originalUrl.replace(nconf.get('relative_path'), '');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
var helpers = module.exports;
 | 
			
		||||
const helpers = module.exports;
 | 
			
		||||
const controllerHelpers = require('../controllers/helpers');
 | 
			
		||||
 | 
			
		||||
helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) {
 | 
			
		||||
	middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks].concat(middlewares);
 | 
			
		||||
@@ -15,8 +16,8 @@ helpers.setupAdminPageRoute = function (router, name, middleware, middlewares, c
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
helpers.setupApiRoute = function (router, name, middleware, middlewares, verb, controller) {
 | 
			
		||||
	router[verb](name, middleware.authenticate, middlewares, helpers.tryRoute(controller, (err, res) => {
 | 
			
		||||
		helpers.formatApiResponse(400, res, err);
 | 
			
		||||
	router[verb](name, middlewares, helpers.tryRoute(controller, (err, res) => {
 | 
			
		||||
		controllerHelpers.formatApiResponse(400, res, err);
 | 
			
		||||
	}));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const router = require('express').Router();
 | 
			
		||||
const middleware = require('../../middleware');
 | 
			
		||||
const controllers = require('../../controllers');
 | 
			
		||||
const routeHelpers = require('../../routes/helpers');
 | 
			
		||||
const routeHelpers = require('../helpers');
 | 
			
		||||
 | 
			
		||||
const setupApiRoute = routeHelpers.setupApiRoute;
 | 
			
		||||
// 	Messaging = require.main.require('./src/messaging'),
 | 
			
		||||
// 	apiMiddleware = require('./middleware'),
 | 
			
		||||
// 	errorHandler = require('../../lib/errorHandler'),
 | 
			
		||||
@@ -10,26 +13,20 @@ const routeHelpers = require('../../routes/helpers');
 | 
			
		||||
// 	utils = require('./utils'),
 | 
			
		||||
// 	async = require.main.require('async');
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
function guestRoutes() {
 | 
			
		||||
	// like registration, login...
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = function () {
 | 
			
		||||
	const router = require('express').Router();
 | 
			
		||||
	const setupApiRoute = routeHelpers.setupApiRoute;
 | 
			
		||||
function authenticatedRoutes() {
 | 
			
		||||
	const middlewares = [middleware.authenticate];
 | 
			
		||||
 | 
			
		||||
	setupApiRoute(router, '/', middleware, [middleware.checkRequired.bind(null, ['username']), middleware.isAdmin], 'post', controllers.write.users.create);
 | 
			
		||||
	setupApiRoute(router, '/', middleware, [...middlewares, middleware.checkRequired.bind(null, ['username']), middleware.isAdmin], 'post', controllers.write.users.create);
 | 
			
		||||
	setupApiRoute(router, '/', middleware, [...middlewares, middleware.checkRequired.bind(null, ['uids']), middleware.isAdmin, middleware.exposePrivileges], 'delete', controllers.write.users.deleteMany);
 | 
			
		||||
	setupApiRoute(router, '/:uid', middleware, [...middlewares], 'put', controllers.write.users.update);
 | 
			
		||||
	setupApiRoute(router, '/:uid', middleware, [...middlewares, middleware.exposePrivileges], 'delete', controllers.write.users.delete);
 | 
			
		||||
 | 
			
		||||
	// 	app.route('/:uid')
 | 
			
		||||
	// 		.put(apiMiddleware.requireUser, apiMiddleware.exposeAdmin, function(req, res) {
 | 
			
		||||
	// 			if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10) && !res.locals.isAdmin) {
 | 
			
		||||
	// 				return errorHandler.respond(401, res);
 | 
			
		||||
	// 			}
 | 
			
		||||
 | 
			
		||||
	// 			// `uid` in `updateProfile` refers to calling user, not target user
 | 
			
		||||
	// 			req.body.uid = req.params.uid;
 | 
			
		||||
 | 
			
		||||
	// 			Users.updateProfile(req.user.uid, req.body, function(err) {
 | 
			
		||||
	// 				return errorHandler.handle(err, res);
 | 
			
		||||
	// 			});
 | 
			
		||||
	// 		})
 | 
			
		||||
	// 		.delete(apiMiddleware.requireUser, apiMiddleware.exposeAdmin, function(req, res) {
 | 
			
		||||
	// 			if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10) && !res.locals.isAdmin) {
 | 
			
		||||
	// 				return errorHandler.respond(401, res);
 | 
			
		||||
@@ -162,6 +159,10 @@ module.exports = function () {
 | 
			
		||||
	// 			errorHandler.handle(err, res);
 | 
			
		||||
	// 		});
 | 
			
		||||
	// 	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = function () {
 | 
			
		||||
	authenticatedRoutes();
 | 
			
		||||
 | 
			
		||||
	return router;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -139,6 +139,8 @@ User.deleteUsersContent = async function (socket, uids) {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
User.deleteUsersAndContent = async function (socket, uids) {
 | 
			
		||||
	sockets.warnDeprecated(socket, 'DELETE /api/v1/users or DELETE /api/v1/users/:uid');
 | 
			
		||||
 | 
			
		||||
	await canDeleteUids(uids);
 | 
			
		||||
	deleteUsers(socket, uids, async function (uid) {
 | 
			
		||||
		return await user.delete(socket.uid, uid);
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,12 @@ const privileges = require('../../privileges');
 | 
			
		||||
const notifications = require('../../notifications');
 | 
			
		||||
const db = require('../../database');
 | 
			
		||||
const plugins = require('../../plugins');
 | 
			
		||||
const sockets = require('..');
 | 
			
		||||
 | 
			
		||||
module.exports = function (SocketUser) {
 | 
			
		||||
	SocketUser.changeUsernameEmail = async function (socket, data) {
 | 
			
		||||
		sockets.warnDeprecated(socket, 'PUT /api/v1/users/:uid');
 | 
			
		||||
 | 
			
		||||
		if (!data || !data.uid || !socket.uid) {
 | 
			
		||||
			throw new Error('[[error:invalid-data]]');
 | 
			
		||||
		}
 | 
			
		||||
@@ -92,6 +95,8 @@ module.exports = function (SocketUser) {
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	SocketUser.updateProfile = async function (socket, data) {
 | 
			
		||||
		sockets.warnDeprecated(socket, 'PUT /api/v1/users/:uid');
 | 
			
		||||
 | 
			
		||||
		if (!socket.uid) {
 | 
			
		||||
			throw new Error('[[error:invalid-uid]]');
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user