diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..2c0e30b9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +.expo +.next +node_modules +package-lock.json +docker* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..c4a007d3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 100 + } + \ No newline at end of file diff --git a/package.json b/package.json index b4f57a92..3d953b77 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "clean": "rimraf lib", "test": "mocha -r @babel/register -r babel-polyfill test/unit/**/*.js", "test:integration": "mocha -r @babel/register -r babel-polyfill test/integration/**/*.js", + "test:auth": "mocha -r @babel/register -r babel-polyfill test/integration/testAuth.js", "test:integration:full": "docker-compose up -d && sleep 10 && mocha -r @babel/register -r babel-polyfill test/integration/**/*.js ; docker-compose down --remove-orphans", "test:prod": "cross-env BABEL_ENV=production npm run test", "test:watch": "npm test -- --watch", @@ -43,6 +44,7 @@ "babel-polyfill": "^6.26.0", "babel-preset-minify": "^0.5.1", "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", "cross-env": "^7.0.2", "jest-websocket-mock": "^2.0.1", "mocha": "^8.0.1", diff --git a/src/Auth.js b/src/Auth.js new file mode 100644 index 00000000..cc6c4875 --- /dev/null +++ b/src/Auth.js @@ -0,0 +1,88 @@ +const superagent = require('superagent') + +class Auth { + constructor(authUrl, supabaseKey, options = { autoRefreshToken: true }) { + this.authUrl = authUrl + this.accessToken = null + this.refreshToken = null + this.supabaseKey = supabaseKey + this.currentUser = null + this.autoRefreshToken = options.autoRefreshToken + + this.signup = async (email, password) => { + const { body } = await superagent + .post(`${authUrl}/signup`, { email, password }) + .set('accept', 'json') + .set('apikey', this.supabaseKey) + + return body + } + + this.login = async (email, password) => { + + const response = await superagent + .post(`${authUrl}/token?grant_type=password`, { email, password }) + .set('accept', 'json') + .set('apikey', this.supabaseKey) + + if (response.status === 200) { + this.accessToken = response.body['access_token'] + this.refreshToken = response.body['refresh_token'] + if (this.autoRefreshToken && tokenExirySeconds) + setTimeout(this.refreshToken, tokenExirySeconds - 60) + } + return response + } + + this.refreshToken = async () => { + const response = await superagent + .post(`${authUrl}/token?grant_type=refresh_token`, { refresh_token: this.refreshToken }) + .set('apikey', this.supabaseKey) + + if (response.status === 200) { + this.accessToken = response.body['access_token'] + this.refreshToken = response.body['refresh_token'] + let tokenExirySeconds = response.body['expires_in'] + if (this.autoRefreshToken && tokenExirySeconds) + setTimeout(this.refreshToken, tokenExirySeconds - 60) + } + return response + } + + this.logout = async () => { + await superagent + .post(`${authUrl}/logout`) + .set('Authorization', `Bearer ${this.accessToken}`) + .set('apikey', this.supabaseKey) + + this.currentUser = null + this.accessToken = null + } + + // this.setRefreshTokenExpiry = (refreshTokenExpirySeconds) => { + // let bufferSeconds = 60 + // let t = new Date() // current time + // this.refreshTokenExpiry = t.setSeconds( + // t.getSeconds() + (refreshTokenExpirySeconds - bufferSeconds) + // ) + // } + + this.user = async () => { + if (this.currentUser) return this.currentUser + + const response = await superagent + .get(`${authUrl}/user`) + .set('Authorization', `Bearer ${this.accessToken}`) + .set('apikey', this.supabaseKey) + + if (response.status === 200) { + this.currentUser = response.body + this.currentUser['access_token'] = this.accessToken + this.currentUser['refresh_token'] = this.refreshToken + } + return this.currentUser + } + } +} + +export { Auth } diff --git a/src/index.js b/src/index.js index a953145d..52360d12 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,15 @@ import { uuid } from './utils/Helpers' import Realtime from './Realtime' +import { Auth } from './Auth' import { PostgrestClient } from '@supabase/postgrest-js' class SupabaseClient { - constructor(supabaseUrl, supabaseKey, options = {}) { + constructor(supabaseUrl, supabaseKey, options = { autoRefreshToken: true }) { this.supabaseUrl = null this.supabaseKey = null this.restUrl = null this.realtimeUrl = null + this.authUrl = null this.schema = 'public' this.subscriptions = {} @@ -17,6 +19,8 @@ class SupabaseClient { if (options.schema) this.schema = options.schema this.authenticate(supabaseUrl, supabaseKey) + + this.auth = new Auth(this.authUrl, supabaseKey, { autoRefreshToken: options.autoRefreshToken }) } /** @@ -28,6 +32,7 @@ class SupabaseClient { this.supabaseKey = supabaseKey this.restUrl = `${supabaseUrl}/rest/v1` this.realtimeUrl = `${supabaseUrl}/realtime/v1`.replace('http', 'ws') + this.authUrl = `${supabaseUrl}/auth/v1` } clear() { @@ -84,8 +89,12 @@ class SupabaseClient { } initClient() { + let headers = { apikey: this.supabaseKey } + + if (this.auth.accessToken) headers['Authorization'] = `Bearer ${this.auth.accessToken}` + let rest = new PostgrestClient(this.restUrl, { - headers: { apikey: this.supabaseKey }, + headers, schema: this.schema, }) let api = rest.from(this.tableName) diff --git a/test/integration/testAuth.js b/test/integration/testAuth.js new file mode 100644 index 00000000..6962392a --- /dev/null +++ b/test/integration/testAuth.js @@ -0,0 +1,34 @@ +const chai = require('chai') +const expect = chai.expect +const assert = chai.assert +chai.use(require('chai-as-promised')) + +import { createClient } from '../../src' + +describe('test signing up and logging in as a new user', () => { + const supabase = createClient( + 'https://HPPNcyqrPOIDwqzQHjRl.supabase.net', + 'JBkDpEMQw9a9yIeVuFhTt5JEhGjQEY' + ) + const randomEmail = `a${Math.random()}@google.com` + + it('should register a new user', async () => { + const response = await supabase.auth.signup(randomEmail, '11password') + assert(response.email === randomEmail, 'user could not sign up') + }) + + it('should log in a user and return an access token', async () => { + const response = await supabase.auth.login(randomEmail, '11password') + assert(response.body.access_token !== undefined, 'user could not log in') + }) + + it('should return the currently logged in user', async () => { + const user = await supabase.auth.user() + assert(user.email === randomEmail, 'user could not be retrieved') + }) + + it('should logout and invalidate the previous access_token', async () => { + await supabase.auth.logout() + await expect(supabase.auth.user()).to.be.rejectedWith(Error) + }) +})