diff --git a/.env.sample b/.env.sample index 0e29b54..9fa5ad2 100644 --- a/.env.sample +++ b/.env.sample @@ -2,3 +2,12 @@ DATABASE_URL= SECRET_KEY= PORT= SENDGRID_API_KEY= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_RETURN_URL= +TWITTER_CONSUMER_KEY = +TWITTER_CONSUMER_SECRET = +TWITTER_RETURN_URL= +FACEBOOK_APP_ID = +FACEBOOK_APP_SECRET = +FACEBOOK_RETURN_URL= \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 8d5807f..efb2771 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,16 @@ cache: script: - export DATABASE_URL="postgres://postgres@127.0.0.1:5432/artemisah" + - export GOOGLE_CLIENT_ID="53636154845-afrnvoq70t0mbceslfrrdegpmobvqcnh.apps.googleusercontent.com" + - export GOOGLE_CLIENT_SECRET="Brnx1TxmaUZlhAk0RJYMmxi_" + - export GOOGLE_RETURN_URL="/api/users/auth/google/redirect" + - export TWITTER_CONSUMER_KEY="fI361EhD3pB8ye7wEQHDezrQi" + - export TWITTER_CONSUMER_SECRET="ItJz0SHSXstsQINs4MMZDkQaAaPAvccfBTeGKEQjQlFcGzau0s" + - export TWITTER_RETURN_URL="/api/users/auth/twitter/redirect" + - export FACEBOOK_APP_ID="775660842814342" + - export FACEBOOK_APP_SECRET="f63a934f47f2499286c251cc530b443c" + - export FACEBOOK_RETURN_URL="/api/users/auth/facebook/redirect" + - export SESSION_SECRET="asdfghjhkjlqwer" - npm test - npm run coveralls diff --git a/package.json b/package.json index 9b57e4d..af2da6e 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "description": "A Social platform for the creative at heart", "main": "index.js", "scripts": { - "start": "babel-node ./server/app.js", + "start": " node ./dist/app.js", "dev": "nodemon --exec babel-node ./server/app.js", "test": "npm run reset:db && nyc mocha --require @babel/polyfill --require @babel/register ./server/test/ --timeout 10000 --exit", "sequelize": "./node_modules/.bin/babel-node ./node_modules/.bin/sequelize $*", "migrate": "./node_modules/.bin/babel-node ./node_modules/.bin/sequelize db:migrate", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "reset:db": "npm run sequelize db:migrate:undo:all && npm run sequelize db:migrate", - "start:dev": "npm run reset:db && npm run dev" + "reset:seeds": "npm run sequelize db:seed:undo:all && npm run sequelize db:seed:all", + "reset:db": "npm run sequelize db:migrate:undo:all && npm run sequelize db:migrate && npm run reset:seeds", + "start:dev": "npm run reset:db && npm run dev", + "build": "rm -rf dist && mkdir dist && babel -d ./dist ./server -s", + "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm run build" }, "author": "Andela Simulations Programme", "license": "MIT", @@ -41,6 +44,9 @@ "nyc": "^13.3.0", "passport": "^0.4.0", "passport-local": "^1.0.0", + "passport-facebook": "^3.0.0", + "passport-google-oauth20": "^1.0.0", + "passport-twitter": "^1.0.4", "pg": "^7.8.1", "request": "^2.87.0", "sendgrid": "^5.2.3", diff --git a/server/app.js b/server/app.js index e9d65bf..ac0c11f 100644 --- a/server/app.js +++ b/server/app.js @@ -2,8 +2,13 @@ import express from 'express'; import bodyParser from 'body-parser'; import logger from 'morgan'; import cors from 'cors'; +import passport from 'passport'; +import dotenv from 'dotenv'; +import expressSession from 'express-session'; import routes from './routes'; +dotenv.config(); + // Set up the express app const app = express(); @@ -16,6 +21,14 @@ app.use(logger('dev')); // Parse incoming requests data app.use(bodyParser.json()); +// Initializing Passport +app.use(passport.initialize()); + +// Creating user session +app.use(expressSession( + { secret: process.env.SESSION_SECRET, resave: true, saveUninitialized: true } +)); + // Setup a default catch-all route that sends back a welcome message in JSON format. app.get('/', (req, res) => res.status(200).send({ message: 'Authors Haven.', diff --git a/server/config/passport.js b/server/config/passport.js index 3b1d769..e9b8ed1 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -1,26 +1,91 @@ -const passport = require("passport"); -const LocalStrategy = require("passport-local").Strategy; -const mongoose = require("mongoose"); -const User = mongoose.model("User"); +import passport from 'passport'; +import passportGoogle from 'passport-google-oauth20'; +import passportTwitter from 'passport-twitter'; +import passportFacebook from 'passport-facebook'; +import dotenv from 'dotenv'; +import db from '../database/models'; + +const { User } = db; + +const GoogleStrategy = passportGoogle.Strategy; +const TwitterStrategy = passportTwitter.Strategy; +const FacebookStrategy = passportFacebook.Strategy; + +dotenv.config(); + +let trustProxy = false; + +if (process.env.DYNO) { + trustProxy = true; +} + +const googleClientId = process.env.GOOGLE_CLIENT_ID; +const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET; +const googleReturnUrl = process.env.GOOGLE_RETURN_URL; +const twitterConsumerKey = process.env.TWITTER_CONSUMER_KEY; +const twitterConsumerSecret = process.env.TWITTER_CONSUMER_SECRET; +const twitterReturnUrl = process.env.TWITTER_RETURN_URL; +const facebookAppId = process.env.FACEBOOK_APP_ID; +const facebookAppSecret = process.env.FACEBOOK_APP_SECRET; +const facebookReturnUrl = process.env.FACEBOOK_RETURN_URL; + +const handleSocialLogin = async (email, firstname, lastname, username, photo, cb) => { + try{ + const existingUser = await User.findOne({ where: { email } }) + return cb(null, { + data: existingUser.dataValues + }); + } + catch{ + const user = await User.create({ + email, + firstname, + lastname, + username: username ? `${username}-${new Date().getTime()}` : `${firstname ? firstname.toLowerCase() : email}${lastname ? lastname.toLowerCase() : ''}-${new Date().getTime()}`, + image: photo, + }) + return cb(null, { data: user.dataValues }) + } +}; passport.use( - new LocalStrategy( - { - usernameField: "user[email]", - passwordField: "user[password]" - }, - function(email, password, done) { - User.findOne({ email: email }) - .then(function(user) { - if (!user || !user.validPassword(password)) { - return done(null, false, { - errors: { "email or password": "is invalid" } - }); - } - - return done(null, user); - }) - .catch(done); - } - ) + new GoogleStrategy({ + clientID: googleClientId, + clientSecret: googleClientSecret, + callbackURL: googleReturnUrl + }, (accessToken, refreshToken, profile, cb) => { + const { name, emails, photos } = profile; + handleSocialLogin(emails[0].value, name.givenName, name.familyName, null, photos[0].value, cb); + }) +); + +passport.use(new TwitterStrategy({ + consumerKey: twitterConsumerKey, + consumerSecret: twitterConsumerSecret, + callbackURL: twitterReturnUrl, + userProfileURL: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true', + includeEmail: true, + proxy: trustProxy +}, +(token, tokenSecret, profile, cb) => { + const { username, emails, photos } = profile; + handleSocialLogin(emails[0].value, null, null, username, photos[0].value, cb); +}) ); + +passport.use(new FacebookStrategy({ + clientID: facebookAppId, + clientSecret: facebookAppSecret, + callbackURL: facebookReturnUrl, + profileFields: ['id', 'displayName', 'photos', 'email'] +}, +(accessToken, refreshToken, profile, cb) => { + const { displayName, photos, emails } = profile; + const splitnames = displayName.split(' '); + const firstname = splitnames[0]; + const lastname = splitnames.length > 1 ? splitnames[1] : ''; + handleSocialLogin(emails[0].value, firstname, lastname, null, photos[0].value, cb); +}) +); + +export default passport; diff --git a/server/controllers/article.js b/server/controllers/article.js index 63ab759..d149105 100644 --- a/server/controllers/article.js +++ b/server/controllers/article.js @@ -3,7 +3,7 @@ import { validationResult } from 'express-validator/check'; import response, { validationErrors } from '../utils/response'; import db from '../database/models'; -const { Article } = db; +const { Article, Tag } = db; class ArticleController { /** @@ -19,7 +19,7 @@ class ArticleController { errors: validationErrors(errors), }); } else { - const { title, description, body } = req.body; + const { title, description, body, tagId } = req.body; let slug = slugify(title, { lower: true, }); @@ -30,6 +30,7 @@ class ArticleController { title, description, body, + tagId }).then((article) => { slug = slug.concat(`-${article.id}`); article.slug = slug; @@ -51,6 +52,25 @@ class ArticleController { }); } } + /** + * Returns all tags + * + * @param {object} req The request object + * @param {object} res The response object + */ + async getTags(req, res){ + try{ + const allTags = await Tag.findAll({ where: {} }); + response(res).success({ + tags: allTags + }) + } + catch(err){ + response(res).serverError({ + message: 'Could not get all tags' + }) + } + } } export default ArticleController; diff --git a/server/controllers/user.js b/server/controllers/user.js index c621c30..f33e8bd 100644 --- a/server/controllers/user.js +++ b/server/controllers/user.js @@ -91,4 +91,39 @@ export default class Users { res.status(400).json({ message: 'invalid email' }); } } + + /** +* @description This controller method completes the social sign in process +* +* @param {object} req - Express request object +* @param {object} res - Express response object +* @return {undefined} +*/ + static async socialLogin(req, res) { + const { data } = req.user; + + try{ + const userToken = await HelperUtils.generateToken(data); + + const { + email, username, bio, image + } = data; + + response(res).success({ + message: 'user logged in successfully', + user: { + email, + username, + bio, + image, + token: userToken, + } + }); + } + catch{ + response(res).serverError({ + message: 'token could not be generated, please try again later' + }); + } + } } diff --git a/server/database/migrations/20190103185917-create-tag.js b/server/database/migrations/20190103185917-create-tag.js new file mode 100644 index 0000000..4756c83 --- /dev/null +++ b/server/database/migrations/20190103185917-create-tag.js @@ -0,0 +1,28 @@ +export default { + up(queryInterface, Sequelize) { + return queryInterface.createTable('Tags', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: new Date().getTime(), + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: new Date().getTime(), + } + }); + }, + down(queryInterface, Sequelize) { + return queryInterface.dropTable('Tags'); + } +}; diff --git a/server/database/migrations/20190227133849-create-article.js b/server/database/migrations/20190227133849-create-article.js index 960277f..64dd019 100644 --- a/server/database/migrations/20190227133849-create-article.js +++ b/server/database/migrations/20190227133849-create-article.js @@ -33,6 +33,14 @@ export default { slug: { type: Sequelize.STRING, }, + tagId: { + type: Sequelize.INTEGER, + references: { + model: 'Tags', + key: 'id', + as: 'tagId' + }, + }, createdAt: { allowNull: false, type: Sequelize.DATE, diff --git a/server/database/models/article.js b/server/database/models/article.js index 0f2a2bb..6d2c2e3 100644 --- a/server/database/models/article.js +++ b/server/database/models/article.js @@ -7,9 +7,14 @@ export default (sequelize, DataTypes) => { primaryImageUrl: DataTypes.STRING, totalClaps: DataTypes.INTEGER, slug: DataTypes.STRING, + tagId: DataTypes.INTEGER }, {}); Article.associate = (models) => { // associations can be defined here + Article.belongsTo(models.Tag, { + foreignKey: 'tagId', + as: 'category' + }); }; return Article; }; diff --git a/server/database/models/tag.js b/server/database/models/tag.js new file mode 100644 index 0000000..a77512d --- /dev/null +++ b/server/database/models/tag.js @@ -0,0 +1,13 @@ +export default (sequelize, DataTypes) => { + const Tag = sequelize.define('Tag', { + name: DataTypes.STRING + }, {}); + Tag.associate = (models) => { + // associations can be defined here + Tag.hasMany(models.Article, { + foreingKey: 'tagId', + as: 'articles' + }); + }; + return Tag; +}; diff --git a/server/database/seeders/20190303192902-add-tags.js b/server/database/seeders/20190303192902-add-tags.js new file mode 100644 index 0000000..f499158 --- /dev/null +++ b/server/database/seeders/20190303192902-add-tags.js @@ -0,0 +1,19 @@ +export default { + up(queryInterface, Sequelize) { + return queryInterface.bulkInsert('Tags', [{ + name: 'Food' + }, { + name: 'Technology' + }, { + name: 'Art' + }, { + name: 'Finance' + }, { + name: 'Health' + }], {}); + }, + + down(queryInterface, Sequelize) { + return queryInterface.bulkDelete('Tags', null, {}); + } +}; diff --git a/server/routes/articles.js b/server/routes/articles.js index 5ad37b2..649986b 100644 --- a/server/routes/articles.js +++ b/server/routes/articles.js @@ -12,4 +12,7 @@ router.post('/articles', createArticleValidation, controller.create.bind(controller)); +router.get('/articles/tags', + controller.getTags.bind(controller)); + export default router; diff --git a/server/routes/users.js b/server/routes/users.js index d6ea6cb..331d16d 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -1,9 +1,29 @@ import express from 'express'; import { Users } from '../controllers'; +import passport from '../config/passport'; const authRoute = express.Router(); authRoute.post('/users', Users.signupUser); authRoute.get('/users/verifyemail', Users.verifyUserEmail); +authRoute.get('/users/auth/google', passport.authenticate('google', { + scope: [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email' + ] +})); + +authRoute.get('/users/auth/google/redirect', passport.authenticate('google', { session: false }), Users.socialLogin); +authRoute.get('/users/auth/facebook', passport.authenticate('facebook', { + scope: ['email'] +})); + +authRoute.get('/users/auth/facebook/redirect', passport.authenticate('facebook', { session: false }), Users.socialLogin); + +authRoute.get('/users/auth/twitter', passport.authenticate('twitter', { + scope: ['include_email=true'] +})); + +authRoute.get('/users/auth/twitter/redirect', passport.authenticate('twitter', { session: false }), Users.socialLogin); export default authRoute; diff --git a/server/test/articles.spec.js b/server/test/articles.spec.js index b6f6ceb..b0f4832 100644 --- a/server/test/articles.spec.js +++ b/server/test/articles.spec.js @@ -50,6 +50,7 @@ describe('Testing articles endpoint', () => { title: 'This is an article', description: 'This is the description of the article', body: 'This is the body of the article', + tagId: 1 }; chai .request(app) @@ -65,7 +66,44 @@ describe('Testing articles endpoint', () => { expect(article.slug).to.equal(`${slugify(data.title, { lower: true })}-${article.id}`); expect(article.description).to.equal('This is the description of the article'); expect(article.body).to.equal('This is the body of the article'); + expect(article.tagId).to.equal(1); + done(); + }); + }); +}); + +describe('Testing Tags Endpoint', () => { + it('should return all tags', (done) => { + chai + .request(app) + .get('/api/articles/tags') + .end((err, res) => { + expect(res.status).to.equal(200); + expect(res.body.tags[0].name).to.equal('Food'); + expect(res.body.tags[1].name).to.equal('Technology'); + expect(res.body.tags[2].name).to.equal('Art'); + expect(res.body.tags[3].name).to.equal('Finance'); + expect(res.body.tags[4].name).to.equal('Health'); + done(); + }); + }); + it('should associate a tag with an article', (done) => { + const data = { + title: 'This is an article', + description: 'This is the description of the article', + body: 'This is the body of the article', + tagId: 1 + }; + chai + .request(app) + .post('/api/articles') + .set('authorization', `Bearer ${token}`) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(201); + const { article } = res.body; + expect(article.tagId).to.equal(1); done(); }); }); diff --git a/server/test/user.spec.js b/server/test/user.spec.js index 1b6df7e..a286989 100644 --- a/server/test/user.spec.js +++ b/server/test/user.spec.js @@ -65,3 +65,28 @@ describe('Test signup endpoint and email verification endpoint', () => { }); }); }); + +describe('Social Login with Google', () => { + it('should return the google authentication webpage', (done) => { + chai + .request(app) + .get(`${signupURL}/auth/google`) + .end((err, res) => { + expect(res.redirects[0]).to.contain('https://accounts.google.com/o/oauth2'); + done(); + }); + }); +}); + +describe('Social Login with Facebook', () => { + it('should return the facebook authentication webpage', (done) => { + chai + .request(app) + .get(`${signupURL}/auth/facebook`) + .end((err, res) => { + expect(res.redirects[0]).to.contain('https://www.facebook.com'); + expect(res.redirects[0]).to.contain('oauth'); + done(); + }); + }); +}); diff --git a/server/utils/response.js b/server/utils/response.js index 67ec336..6911efe 100644 --- a/server/utils/response.js +++ b/server/utils/response.js @@ -61,13 +61,22 @@ export default (res) => { /** * Sends status 400 and `data` to the client - * + * * @param {*} data */ badRequest(data) { this.sendData(400, data); }, + /** + * Sends status 500 and `data` to the client + * Should be sent when there's a server error + * @param {*} data + */ + serverError(data) { + this.sendData(500, data); + }, + /** * Send data to the client. * diff --git a/swagger.yaml b/swagger.yaml index d42c9d9..a6280ca 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,265 +1,235 @@ -swagger: "2.0" +swagger: '2.0' info: - description: "A Social platform for the creative at heart" - version: "1.0.0" - title: "Author's Haven" - termsOfService: "http://swagger.io/terms/" + description: A Social platform for the creative at heart + version: 1.0.0 + title: Author's Haven + termsOfService: 'http://swagger.io/terms/' contact: - email: "apiteam@swagger.io" + email: apiteam@swagger.io + name: Artemis license: - name: "Apache 2.0" - url: "http://www.apache.org/licenses/LICENSE-2.0.html" -host: "authorshaven.herokuapp.com" -basePath: "/api" + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +host: authorshaven.herokuapp.com +basePath: /api tags: -- name: "pet" - description: "Everything about your Pets" - externalDocs: - description: "Find out more" - url: "http://swagger.io" -- name: "store" - description: "Access to Petstore orders" -- name: "article" - description: "Every thing relating to articles" + - name: user + description: Everything associated to users + - name: profile + description: Every thing relating to the user profile + - name: article + description: Every thing relating to articles schemes: -- "https" + - https paths: - /profiles/{username}: + /users: + post: + tags: + - user + summary: Creates a new user + description: This endpoint signup a new user + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: user + description: User details + required: true + schema: + type: object + properties: + firstname: + type: string + lastname: + type: string + username: + type: string + email: + type: string + password: + type: string + responses: + '200': + description: user created successfully + /users/verifyemail: get: tags: - - "pet" - summary: "Returns a Profile" - description: "" - operationId: "addPet" + - user + summary: Verify a new user's email + description: This endpoint verifies a new user's email consumes: - - "application/json" + - application/json produces: - - "application/json" + - application/json parameters: - - name: "username" - in: "path" - description: "Username of user to return" - required: true - type: "string" + - name: email + in: path + description: New user's email address + required: true + type: string + - name: hash + in: path + description: Encrypted data for comparism + required: true + type: string responses: - 200: - description: "Success" - 405: - description: "Invalid input" - security: - - petstore_auth: - - "write:pets" - - "read:pets" - + '200': + description: email verified successfully + '400': + description: invalid email /articles: post: tags: - - "article" - summary: "Creates a new article" - description: "Creates a new article" + - article + summary: Creates a new article + description: Creates a new article consumes: - - "application/json" + - application/json + produces: + - application/json + parameters: + - in: body + name: article + description: Create an article + required: true + schema: + type: object + properties: + title: + type: string + description: + type: string + body: + type: string + responses: + '200': + description: Success + security: + - authorization: [] + /articles/tags: + get: + tags: + - "article" + summary: "Returns all tags" + description: "Returns all the tags an article could have" produces: - "application/json" + responses: + 200: + description: Success + + /profiles/followers: + get: + tags: + - profile + summary: fetch all authors following you + description: This endpoint gets all your followers + produces: + - application/json + responses: + '200': + description: these are your followers + security: + - authorization: [] + /profiles/following: + get: + tags: + - profile + summary: fetch all authors you are following + description: This endpoint gets all authors you are following + produces: + - application/json + responses: + '200': + description: people you are following + security: + - authorization: [] + /profiles/{username}/follow: + get: + tags: + - profile + summary: Follow an author by username + description: Follows a single author + produces: + - application/xml + - application/json + parameters: + - name: username + in: path + description: username of author to return + required: true + type: string + responses: + '200': + description: 'you already following ${authors username}' + '201': + description: 'you just followed ${authors username}' + '403': + description: you cannot follow yourself + security: + - authorization: [] + delete: + tags: + - profile + summary: Unfollow an author by username + description: Unfollows a single author + produces: + - application/xml + - application/json parameters: - - in: "body" - name: "article" - description: "Create an article" - required: true - schema: - type: "object" - properties: - title: - type: string - description: - type: string - body: - type: string + - name: username + in: path + description: username of author to return + required: true + type: string + responses: + '200': + description: '${authors username} has been unfollowed' + '403': + description: you cannot unfollow yourself + '404': + description: user not found + security: + - authorization: [] + /users/auth/google: + get: + tags: + - "users" + summary: "Signs users in with google" + description: "Redirects to google authentication page to seek their permission to share their information with the platform" + produces: + - "text/html" responses: 200: - description: "Success" - - /users: - post: - tags: - - "users" - summary: "Signs up a new user" - description: "Creates a user" - consumes: - - "application/json" - produces: - - "application/json" - parameters: - - in: "body" - name: "users" - description: "Create a user" - required: true - schema: - type: "object" - properties: - firstname: - type: string - lastname: - type: string - username: - type: string - email: - type: string - password: - type: string - responses: - 200: - description: "user created successfully" + description: "user logged in successfully" + /users/auth/facebook: + get: + tags: + - "users" + summary: "Signs users in with facebook" + description: "Redirects to facebook authentication page to seek their permission to share their information with the platform" + produces: + - "text/html" + responses: + 200: + description: "user logged in successfully" - /users/verifyemail: - get: - tags: - - "users" - summary: "Verifies a new user" - description: "Verifies a new users email" - consumes: - - "application/json" - produces: - - "application/json" - parameters: - - in: "body" - responses: - 200: - description: "email verified successfully" - 400: - description: "invalid email" - -securityDefinitions: - petstore_auth: - type: "oauth2" - authorizationUrl: "http://petstore.swagger.io/oauth/dialog" - flow: "implicit" - scopes: - write:pets: "modify pets in your account" - read:pets: "read your pets" - api_key: - type: "apiKey" - name: "api_key" - in: "header" -definitions: - Order: - type: "object" - properties: - id: - type: "integer" - format: "int64" - petId: - type: "integer" - format: "int64" - quantity: - type: "integer" - format: "int32" - shipDate: - type: "string" - format: "date-time" - status: - type: "string" - description: "Order Status" - enum: - - "placed" - - "approved" - - "delivered" - complete: - type: "boolean" - default: false - xml: - name: "Order" - Category: - type: "object" - properties: - id: - type: "integer" - format: "int64" - name: - type: "string" - xml: - name: "Category" - User: - type: "object" - properties: - id: - type: "integer" - format: "int64" - username: - type: "string" - firstName: - type: "string" - lastName: - type: "string" - email: - type: "string" - password: - type: "string" - phone: - type: "string" - userStatus: - type: "integer" - format: "int32" - description: "User Status" - xml: - name: "User" - Tag: - type: "object" - properties: - id: - type: "integer" - format: "int64" - name: - type: "string" - xml: - name: "Tag" - Pet: - type: "object" - required: - - "name" - - "photoUrls" - properties: - id: - type: "integer" - format: "int64" - category: - $ref: "#/definitions/Category" - name: - type: "string" - example: "doggie" - photoUrls: - type: "array" - xml: - name: "photoUrl" - wrapped: true - items: - type: "string" + /users/auth/twitter: + get: tags: - type: "array" - xml: - name: "tag" - wrapped: true - items: - $ref: "#/definitions/Tag" - status: - type: "string" - description: "pet status in the store" - enum: - - "available" - - "pending" - - "sold" - xml: - name: "Pet" - ApiResponse: - type: "object" - properties: - code: - type: "integer" - format: "int32" - type: - type: "string" - message: - type: "string" -externalDocs: - description: "Find out more about Swagger" - url: "http://swagger.io" + - "users" + summary: "Signs users in with twitter" + description: "Redirects to twitter authentication page to seek their permission to share their information with the platform" + produces: + - "text/html" + responses: + 200: + description: "user logged in successfully" + +securityDefinitions: + authorization: + type: apiKey + name: authorization + in: header