diff --git a/.adonisrc.json b/.adonisrc.json deleted file mode 100644 index 9836545..0000000 --- a/.adonisrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "typescript": true, - "providers": ["@adonisjs/core", "./providers/AllyProvider"], - "preloads": [ - "./examples/discord", - "./examples/github", - "./examples/twitter", - "./examples/google", - "./examples/linkedin", - "./examples/facebook", - "./examples/spotify" - ], - "directories": { - "config": "./examples/config" - } -} diff --git a/.bin/test.js b/.bin/test.js deleted file mode 100644 index e20c2dd..0000000 --- a/.bin/test.js +++ /dev/null @@ -1,7 +0,0 @@ -require('@adonisjs/require-ts/build/register') - -const { configure } = require('japa') - -configure({ - files: ['test/**/*.spec.ts'], -}) diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md deleted file mode 100644 index fc852af..0000000 --- a/.github/COMMIT_CONVENTION.md +++ /dev/null @@ -1,70 +0,0 @@ -## Git Commit Message Convention - -> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). - -Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. - -``` js -/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ -``` - -## Commit Message Format -A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: - -> The **scope** is optional - -``` -feat(router): add support for prefix - -Prefix makes it easier to append a path to a group of routes -``` - -1. `feat` is type. -2. `router` is scope and is optional -3. `add support for prefix` is the subject -4. The **body** is followed by a blank line. -5. The optional **footer** can be added after the body, followed by a blank line. - -## Types -Only one type can be used at a time and only following types are allowed. - -- feat -- fix -- docs -- style -- refactor -- perf -- test -- workflow -- ci -- chore -- types -- build - -If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. - -### Revert -If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. - -## Scope -The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. - -## Subject -The subject contains succinct description of the change: - -- use the imperative, present tense: "change" not "changed" nor "changes". -- don't capitalize first letter -- no dot (.) at the end - -## Body - -Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". -The body should include the motivation for the change and contrast this with previous behavior. - -## Footer - -The footer should contain any information about **Breaking Changes** and is also the place to -reference GitHub issues that this commit **Closes**. - -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. - diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index f0c5446..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributing - -AdonisJS is a community driven project. You are free to contribute in any of the following ways. - -- [Coding style](coding-style) -- [Fix bugs by creating PR's](fix-bugs-by-creating-prs) -- [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) -- [Report security issues](report-security-issues) -- [Be a part of the community](be-a-part-of-community) - -## Coding style - -Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. - -## Fix bugs by creating PR's - -We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. - -Go through the following points, before creating a new PR. - -1. Create an issue discussing the bug or short-coming in the framework. -2. Once approved, go ahead and fork the REPO. -3. Make sure to start from the `develop`, since this is the upto date branch. -4. Make sure to keep commits small and relevant. -5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. -6. Once done with all the changes, create a PR against the `develop` branch. - -## Share an RFC for new features or big changes - -Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. - -### What is an RFC? - -RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). - -In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. - -The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. - -## Report security issues - -All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. - -## Be a part of community - -We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e65000c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Report identified bugs ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -- Lots of raised issues are directly not bugs but instead are design decisions taken by us. -- Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. -- Ensure the issue isn't already reported. -- Ensure you are reporting the bug in the correct repo. - -*Delete the above section and the instructions in the sections below before submitting* - -## Package version - - -## Node.js and npm version - - -## Sample Code (to reproduce the issue) - - -## BONUS (a sample repo to reproduce the issue) - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index abd44a5..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature request -about: Propose changes for adding a new feature ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -## Consider an RFC - -Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if - -- Feature introduces a breaking change -- Demands lots of time and changes in the current code base. - -*Delete the above section and the instructions in the sections below before submitting* - -## Why this feature is required (specific use-cases will be appreciated)? - - -## Have you tried any other work arounds? - - -## Are you willing to work on it with little guidance? - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index cceec95..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Proposed changes - -Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. - -## Types of changes - -What types of changes does your code introduce? - -_Put an `x` in the boxes that apply_ - -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -## Checklist - -_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - -- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/ally/blob/master/.github/CONTRIBUTING.md) doc -- [ ] Lint and unit tests pass locally with my changes -- [ ] I have added tests that prove my fix is effective or that my feature works. -- [ ] I have added necessary documentation (if appropriate) - -## Further comments - -If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..ba001c6 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,170 @@ +[ + { + "name": "Priority: Critical", + "color": "ea0056", + "description": "The issue needs urgent attention", + "aliases": [] + }, + { + "name": "Priority: High", + "color": "5666ed", + "description": "Look into this issue before picking up any new work", + "aliases": [] + }, + { + "name": "Priority: Medium", + "color": "f4ff61", + "description": "Try to fix the issue for the next patch/minor release", + "aliases": [] + }, + { + "name": "Priority: Low", + "color": "87dfd6", + "description": "Something worth considering, but not a top priority for the team", + "aliases": [] + }, + { + "name": "Semver: Alpha", + "color": "008480", + "description": "Will make it's way to the next alpha version of the package", + "aliases": [] + }, + { + "name": "Semver: Major", + "color": "ea0056", + "description": "Has breaking changes", + "aliases": [] + }, + { + "name": "Semver: Minor", + "color": "fbe555", + "description": "Mainly new features and improvements", + "aliases": [] + }, + { + "name": "Semver: Next", + "color": "5666ed", + "description": "Will make it's way to the bleeding edge version of the package", + "aliases": [] + }, + { + "name": "Semver: Patch", + "color": "87dfd6", + "description": "A bug fix", + "aliases": [] + }, + { + "name": "Status: Abandoned", + "color": "ffffff", + "description": "Dropped and not into consideration", + "aliases": ["wontfix"] + }, + { + "name": "Status: Accepted", + "color": "e5fbf2", + "description": "The proposal or the feature has been accepted for the future versions", + "aliases": [] + }, + { + "name": "Status: Blocked", + "color": "ea0056", + "description": "The work on the issue or the PR is blocked. Check comments for reasoning", + "aliases": [] + }, + { + "name": "Status: Completed", + "color": "008672", + "description": "The work has been completed, but not released yet", + "aliases": [] + }, + { + "name": "Status: In Progress", + "color": "73dbc4", + "description": "Still banging the keyboard", + "aliases": ["in progress"] + }, + { + "name": "Status: On Hold", + "color": "f4ff61", + "description": "The work was started earlier, but is on hold now. Check comments for reasoning", + "aliases": ["On Hold"] + }, + { + "name": "Status: Review Needed", + "color": "fbe555", + "description": "Review from the core team is required before moving forward", + "aliases": [] + }, + { + "name": "Status: Awaiting More Information", + "color": "89f8ce", + "description": "Waiting on the issue reporter or PR author to provide more information", + "aliases": [] + }, + { + "name": "Status: Need Contributors", + "color": "7057ff", + "description": "Looking for contributors to help us move forward with this issue or PR", + "aliases": [] + }, + { + "name": "Type: Bug", + "color": "ea0056", + "description": "The issue has indentified a bug", + "aliases": ["bug"] + }, + { + "name": "Type: Security", + "color": "ea0056", + "description": "Spotted security vulnerability and is a top priority for the core team", + "aliases": [] + }, + { + "name": "Type: Duplicate", + "color": "00837e", + "description": "Already answered or fixed previously", + "aliases": ["duplicate"] + }, + { + "name": "Type: Enhancement", + "color": "89f8ce", + "description": "Improving an existing feature", + "aliases": ["enhancement"] + }, + { + "name": "Type: Feature Request", + "color": "483add", + "description": "Request to add a new feature to the package", + "aliases": [] + }, + { + "name": "Type: Invalid", + "color": "dbdbdb", + "description": "Doesn't really belong here. Maybe use discussion threads?", + "aliases": ["invalid"] + }, + { + "name": "Type: Question", + "color": "eceafc", + "description": "Needs clarification", + "aliases": ["help wanted", "question"] + }, + { + "name": "Type: Documentation Change", + "color": "7057ff", + "description": "Documentation needs some improvements", + "aliases": ["documentation"] + }, + { + "name": "Type: Dependencies Update", + "color": "00837e", + "description": "Bump dependencies", + "aliases": ["dependencies"] + }, + { + "name": "Good First Issue", + "color": "008480", + "description": "Want to contribute? Just filter by this label", + "aliases": ["good first issue"] + } +] diff --git a/.github/stale.yml b/.github/stale.yml index 7a6a571..f767674 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,10 +6,10 @@ daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - - "Type: Security" + - 'Type: Security' # Label to use when marking an issue as stale -staleLabel: "Status: Abandoned" +staleLabel: 'Status: Abandoned' # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..c27fb04 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,14 @@ +name: checks +on: + - push + - pull_request + +jobs: + test: + uses: adonisjs/.github/.github/workflows/test.yml@main + + lint: + uses: adonisjs/.github/.github/workflows/lint.yml@main + + typecheck: + uses: adonisjs/.github/.github/workflows/typecheck.yml@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 8b778ca..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: test -on: - - push - - pull_request -jobs: - linux: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: - - 14.15.4 - - 17.x - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install - run: npm install - - name: Run tests - run: npm test diff --git a/.husky/commit-msg b/.husky/commit-msg index 4654c12..4002db7 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,3 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit diff --git a/LICENSE.md b/LICENSE.md index 1c19428..381426b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License -Copyright 2022 Harminder Virk, contributors +Copyright (c) 2023 Harminder Virk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 02fdcd6..2b979e6 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,33 @@ -
- -
+# @adonisjs/ally
-
-

Social Authentication

-

Social authentication provider for AdonisJS. Supports Github, Google, Twitter, Facebook, Discord, Spotify, and LinkedIn.

-
+[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] -
- -
+## Introduction +Social authentication provider for AdonisJS. Supports **Github**, **Google**, **Twitter**, **Facebook**, **Discord**, **Spotify**, and **LinkedIn**. -[![gh-workflow-image]][gh-workflow-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] [![synk-image]][synk-url] +## Official Documentation +The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/social_auth) -
+## Contributing +One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. -
-

- - Website - - | - - Guides - - | - - Contributing - -

-
+We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. -
- Built with ❤︎ by Harminder Virk -
+## Code of Conduct +In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). -[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/ally/test?style=for-the-badge -[gh-workflow-url]: https://github.com/adonisjs/ally/actions/workflows/test.yml "Github action" +## License +AdonisJS ally is open-sourced software licensed under the [MIT license](LICENSE.md). -[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript -[typescript-url]: "typescript" +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/ally/checks.yml?style=for-the-badge +[gh-workflow-url]: https://github.com/adonisjs/ally/actions/workflows/checks.yml "Github action" [npm-image]: https://img.shields.io/npm/v/@adonisjs/ally/latest.svg?style=for-the-badge&logo=npm [npm-url]: https://www.npmjs.com/package/@adonisjs/ally/v/latest "npm" -[license-image]: https://img.shields.io/npm/l/@adonisjs/ally?color=blueviolet&style=for-the-badge -[license-url]: LICENSE.md "license" +[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript -[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/ally?label=Synk%20Vulnerabilities&style=for-the-badge -[synk-url]: https://snyk.io/test/github/adonisjs/ally?targetFile=package.json "synk" +[license-url]: LICENSE.md +[license-image]: https://img.shields.io/github/license/adonisjs/ally?style=for-the-badge diff --git a/adonis-typings/ally.ts b/adonis-typings/ally.ts deleted file mode 100644 index 18e95ed..0000000 --- a/adonis-typings/ally.ts +++ /dev/null @@ -1,692 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Addons/Ally' { - import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - import { ApplicationContract } from '@ioc:Adonis/Core/Application' - import { - Oauth2AccessToken, - Oauth1RequestToken, - Oauth1AccessToken, - Oauth1ClientConfig, - Oauth2ClientConfig, - ApiRequestContract, - RedirectRequestContract as ClientRequestContract, - } from '@poppinss/oauth-client' - - export { Oauth2AccessToken } - export { Oauth1AccessToken } - export { Oauth1RequestToken } - export { ApiRequestContract } - export { Oauth2ClientConfig as Oauth2DriverConfig } - export { Oauth1ClientConfig as Oauth1DriverConfig } - - /** - * Issue: https://github.com/Microsoft/TypeScript/issues/29729 - * Solution: https://github.com/sindresorhus/type-fest/blob/main/source/literal-union.d.ts - */ - export type LiteralStringUnion = LiteralType | (string & { _?: never }) - - /** - * Extension of oauth-client redirect request with support - * for defining scopes as first class citizen - */ - export interface RedirectRequestContract - extends ClientRequestContract { - /** - * Define a callback to transform scopes before they are defined - * as a param - */ - transformScopes(callback: (scopes: LiteralStringUnion[]) => string[]): this - - /** - * Define the scopes for authorization - */ - scopes(scopes: LiteralStringUnion[]): this - - /** - * Merge to existing pre-defined scopes - */ - mergeScopes(scopes: LiteralStringUnion[]): this - - /** - * Clear existing scopes - */ - clearScopes(): this - } - - /** - * The user fetched from the oauth provider. - */ - export interface AllyUserContract { - id: string - nickName: string - name: string - email: string | null - emailVerificationState: 'verified' | 'unverified' | 'unsupported' - avatarUrl: string | null - token: Token - original: any - } - - /** - * Every driver should implement this contract - */ - export interface AllyDriverContract< - Token extends Oauth2AccessToken | Oauth1AccessToken, - Scopes extends string - > { - version: 'oauth1' | 'oauth2' - - /** - * Perform stateless authentication. Only applicable for Oauth2 clients - */ - stateless(): this - - /** - * Redirect user for authorization - */ - redirect(callback?: (request: RedirectRequestContract) => void): Promise - - /** - * Get redirect url. You must manage the state yourself when redirecting - * manually - */ - redirectUrl(callback?: (request: RedirectRequestContract) => void): Promise - - /** - * Find if the current request has authorization code or oauth token - */ - hasCode(): boolean - - /** - * Get the current request authorization code or oauth token. Returns - * null if there no code - */ - getCode(): string | null - - /** - * Find if the current error code is for access denied - */ - accessDenied(): boolean - - /** - * Find if there is a state mismatch - */ - stateMisMatch(): boolean - - /** - * Find if there is an error post redirect - */ - hasError(): boolean - - /** - * Get the post redirect error - */ - getError(): string | null - - /** - * Get access token - */ - accessToken(callback?: (request: ApiRequestContract) => void): Promise - - /** - * Returns details for the authorized user - */ - user(callback?: (request: ApiRequestContract) => void): Promise> - - /** - * Finds the user by access token. Applicable with "Oauth2" only - */ - userFromToken( - token: string, - callback?: (request: ApiRequestContract) => void - ): Promise> - - /** - * Finds the user by access token. Applicable with "Oauth1" only - */ - userFromTokenAndSecret( - token: string, - secret: string, - callback?: (request: ApiRequestContract) => void - ): Promise> - } - - /** - * ---------------------------------------- - * Discord driver - * ---------------------------------------- - */ - - /** - * Available discord scopes - * https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes - */ - export type DiscordScopes = - | 'activities.read' - | 'activities.write' - | 'applications.builds.read' - | 'applications.builds.upload' - | 'applications.commands' - | 'applications.commands.update' - | 'applications.entitlements' - | 'applications.store.update' - | 'bot' - | 'connections' - | 'email' - | 'gdm.join' - | 'guilds' - | 'guilds.join' - | 'identify' - | 'messages.read' - | 'relationships.read' - | 'rpc' - | 'rpc.activities.write' - | 'rpc.notifications.read' - | 'rpc.voice.read' - | 'rpc.voice.write' - | 'webhook.incoming' - - /** - * Shape of the Discord access token - */ - export type DiscordToken = { - token: string - type: string - scope: string - expiresIn: number - expiresAt: Exclude - refreshToken: string - } - - /** - * Extra options available for Discord - */ - export type DiscordDriverConfig = Oauth2ClientConfig & { - driver: 'discord' - userInfoUrl?: string - scopes?: LiteralStringUnion[] - prompt?: 'consent' | 'none' - guildId?: `${bigint}` // a snowflake - disableGuildSelect?: boolean - permissions?: number - } - - export interface DiscordDriverContract extends AllyDriverContract { - version: 'oauth2' - } - - /** - * ---------------------------------------- - * Github driver - * ---------------------------------------- - */ - - /** - * Available github scopes - * https://docs.github.com/en/free-pro-team@latest/developers/apps/scopes-for-oauth-apps#available-scopes - */ - export type GithubScopes = - | 'repo' - | 'repo:status' - | 'repo_deployment' - | 'public_repo' - | 'repo:invite' - | 'security_events' - | 'admin:repo_hook' - | 'write:repo_hook' - | 'read:repo_hook' - | 'admin:org' - | 'write:org' - | 'read:org' - | 'admin:public_key' - | 'write:public_key' - | 'read:public_key' - | 'admin:org_hook' - | 'gist' - | 'notifications' - | 'user' - | 'read:user' - | 'user:email' - | 'user:follow' - | 'delete_repo' - | 'write:discussion' - | 'read:discussion' - | 'write:packages' - | 'read:packages' - | 'delete:packages' - | 'admin:gpg_key' - | 'write:gpg_key' - | 'read:gpg_key' - | 'workflow' - - /** - * Shape of the Github access token - */ - export type GithubToken = { - token: string - type: string - scope: string - } - - /** - * Extra options available for Github - */ - export type GithubDriverConfig = Oauth2ClientConfig & { - driver: 'github' - login?: string - scopes?: LiteralStringUnion[] - allowSignup?: boolean - userInfoUrl?: string - userEmailUrl?: string - } - - export interface GithubDriverContract extends AllyDriverContract { - version: 'oauth2' - } - - /** - * ---------------------------------------- - * Twitter driver - * ---------------------------------------- - */ - - /** - * Shape of the twitter token - */ - export type TwitterToken = { - token: string - secret: string - userId: string - screenName: string - } - - /** - * Extra options available for twitter - */ - export type TwitterDriverConfig = Oauth1ClientConfig & { - driver: 'twitter' - userInfoUrl?: string - } - - export interface TwitterDriverContract extends AllyDriverContract { - version: 'oauth1' - } - - /** - * ---------------------------------------- - * Google driver - * ---------------------------------------- - */ - - /** - * Most popular google scopes. You can find rest of them on the following link - * https://developers.google.com/identity/protocols/oauth2/scopes - */ - export type GoogleScopes = - | 'userinfo.email' - | 'userinfo.profile' - | 'openid' - | 'contacts' - | 'contacts.other.readonly' - | 'contacts.readonly' - | 'directory.readonly' - | 'user.addresses.read' - | 'user.birthday.read' - | 'user.emails.read' - | 'user.gender.read' - | 'user.organization.read' - | 'user.phonenumbers.read' - | 'analytics' - | 'analytics.readonly' - | 'documents' - | 'documents.readonly' - | 'forms' - | 'forms.currentonly' - | 'groups' - | 'spreadsheets' - | 'calendar' - | 'calendar.events' - | 'calendar.events.readonly' - | 'calendar.readonly' - | 'calendar.settings.readonly' - | 'drive' - | 'drive.appdata' - | 'drive.file' - | 'drive.metadata' - | 'drive.metadata.readonly' - | 'drive.photos.readonly' - | 'drive.readonly' - | 'drive.scripts' - - /** - * Shape of the Google access token - */ - export type GoogleToken = Oauth2AccessToken & { - /** - * @deprecated - * Use `idToken` instead - */ - id_token: string - idToken: string - grantedScopes: string[] - } - - /** - * Config accepted by the google driver. Most of the options can be - * overwritten at runtime - * https://developers.google.com/identity/protocols/oauth2/openid-connect#re-consent - */ - export type GoogleDriverConfig = Oauth2ClientConfig & { - driver: 'google' - userInfoUrl?: string - - /** - * Can be configured at runtime - */ - scopes?: LiteralStringUnion[] - prompt?: 'none' | 'consent' | 'select_account' - accessType?: 'online' | 'offline' - hostedDomain?: string - display?: 'page' | 'popup' | 'touch' | 'wrap' - } - - export interface GoogleDriverContract extends AllyDriverContract { - version: 'oauth2' - } - - /** - * ---------------------------------------- - * LinkedIn driver - * ---------------------------------------- - */ - - /** - * A list of LinkedIn scopes. You can find the scopes available - * to your app from the LinkedIn dasbhoard. - * https://www.linkedin.com/developers/apps//auth - */ - export type LinkedInScopes = - | 'r_emailaddress' - | 'r_liteprofile' - | 'w_member_social' - | 'r_fullprofile' - | 'r_basicprofile_app' - | 'r_primarycontact' - | 'rw_organization_admin' - - /** - * Shape of the Linked access token - */ - export type LinkedInToken = { - token: string - type: string - expiresIn: number - expiresAt: Exclude - } - - /** - * Config accepted by the linkedin driver. Most of the options can be - * overwritten at runtime - * https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext&tabs=HTTPS#step-2-request-an-authorization-code - */ - export type LinkedInDriverConfig = Oauth2ClientConfig & { - driver: 'linkedin' - userInfoUrl?: string - userEmailUrl?: string - - /** - * Can be configured at runtime - */ - scopes?: LiteralStringUnion[] - } - - export interface LinkedInDriverContract - extends AllyDriverContract { - version: 'oauth2' - } - - /** - * ---------------------------------------- - * Facebook driver - * ---------------------------------------- - */ - - /** - * Most popular facebook scopes. You can find rest of them on the following link - * https://developers.facebook.com/docs/permissions/reference/ - */ - export type FacebookScopes = - | 'ads_management' - | 'ads_read' - | 'attribution_read' - | 'business_management' - | 'catalog_management' - | 'email' - | 'groups_access_member_info' - | 'leads_retrieval' - | 'pages_events' - | 'pages_manage_ads' - | 'pages_manage_cta' - | 'pages_manage_instant_articles' - | 'pages_manage_engagement' - | 'pages_manage_metadata' - | 'pages_manage_posts' - | 'pages_messaging' - | 'pages_read_engagement' - | 'pages_read_user_content' - | 'pages_show_list' - | 'pages_user_gender' - | 'pages_user_locale' - | 'pages_user_timezone' - | 'public_profile' - | 'publish_to_groups' - | 'publish_video' - | 'read_insights' - | 'user_age_range' - | 'user_birthday' - | 'user_friends' - | 'user_gender' - | 'user_hometown' - | 'user_likes' - | 'user_link' - | 'user_location' - | 'user_photos' - | 'user_posts' - | 'user_videos' - - /** - * Most used user profile fields. - * For more visit https://developers.facebook.com/docs/graph-api/reference/user - */ - export type FacebookProfileFields = - | 'id' - | 'first_name' - | 'last_name' - | 'middle_name' - | 'name' - | 'name_format' - | 'picture' - | 'short_name' - | 'verified' - | 'birthday' - | 'email' - | 'gender' - | 'link' - - /** - * Shape of the Facebook access token - */ - export type FacebookToken = { - token: string - type: string - expiresIn: number - expiresAt: Exclude - } - - /** - * Config accepted by the facebook driver. Most of the options can be - * overwritten at runtime - * https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow - */ - export type FacebookDriverConfig = Oauth2ClientConfig & { - driver: 'facebook' - userInfoUrl?: string - - /** - * Can be configured at runtime - */ - scopes?: LiteralStringUnion[] - userFields?: LiteralStringUnion[] - display?: string - authType?: string - } - - export interface FacebookDriverContract - extends AllyDriverContract { - version: 'oauth2' - } - - /** - * ---------------------------------------- - * Spotify driver - * ---------------------------------------- - */ - - /** - * Available spotify scopes - * https://developer.spotify.com/documentation/general/guides/scopes/ - */ - export type SpotifyScopes = - | 'ugc-image-upload' - | 'user-read-recently-played' - | 'user-top-read' - | 'user-read-playback-position' - | 'user-read-playback-state' - | 'user-modify-playback-state' - | 'user-read-currently-playing' - | 'app-remote-control' - | 'streaming' - | 'playlist-modify-public' - | 'playlist-modify-private' - | 'playlist-read-private' - | 'playlist-read-collaborative' - | 'user-follow-modify' - | 'user-follow-read' - | 'user-library-modify' - | 'user-library-read' - | 'user-read-email' - | 'user-read-private' - - /** - * Shape of the Spotify access token - */ - export type SpotifyToken = { - token: string - type: string - refreshToken: string - expiresIn: number - expiresAt: Exclude - } - - /** - * Extra options available for Spotify - */ - export type SpotifyDriverConfig = Oauth2ClientConfig & { - driver: 'spotify' - scopes?: LiteralStringUnion[] - showDialog?: boolean - } - - export interface SpotifyDriverContract extends AllyDriverContract { - version: 'oauth2' - } - - /** - * END OF DRIVERS - */ - - /** - * Must be defined in user land - */ - export interface SocialProviders {} - - /** - * Ally config - */ - export type AllyConfig = { - [Provider in keyof SocialProviders]: SocialProviders[Provider]['config'] - } - - /** - * Shape of the callback to extend the ally drivers - */ - export type ExtendDriverCallback = ( - ally: AllyManagerContract, - mapping: string, - config: any, - ctx: HttpContextContract - ) => AllyDriverContract - - /** - * Ally instance is passed to every HTTP request and has access to the - * current request - */ - export interface AllyContract { - /** - * Get driver instance of a named mapping - */ - use( - provider: Provider - ): SocialProviders[Provider]['implementation'] - - /** - * Get driver instance of an unnamed mapping - */ - use(provider: string): AllyDriverContract - } - - /** - * Ally Manager manages the lifecycle of Ally drivers - */ - export interface AllyManagerContract { - application: ApplicationContract - - /** - * Make instance of a named mapping - */ - makeMapping( - ctx: HttpContextContract, - mapping: keyof SocialProviders - ): SocialProviders[Provider]['implementation'] - - /** - * Make instance of an unnamed mapping - */ - makeMapping( - ctx: HttpContextContract, - mapping: string - ): AllyDriverContract - - /** - * Returns an instance of ally, which can be later used to - * get instances of social providers for a given request - */ - getAllyForRequest(ctx: HttpContextContract): AllyContract - - /** - * Extend ally by adding new drivers - */ - extend(driverName: string, callback: ExtendDriverCallback): void - } - - const Ally: AllyManagerContract - export default Ally -} diff --git a/adonis-typings/container.ts b/adonis-typings/container.ts deleted file mode 100644 index f6c069d..0000000 --- a/adonis-typings/container.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/Application' { - import { AllyManagerContract } from '@ioc:Adonis/Addons/Ally' - export interface ContainerBindings { - 'Adonis/Addons/Ally': AllyManagerContract - } -} diff --git a/adonis-typings/context.ts b/adonis-typings/context.ts deleted file mode 100644 index 6eb542d..0000000 --- a/adonis-typings/context.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/HttpContext' { - import { AllyContract } from '@ioc:Adonis/Addons/Ally' - export interface HttpContextContract { - ally: AllyContract - } -} diff --git a/bin/japaTypes.ts b/bin/japaTypes.ts deleted file mode 100644 index d42cac6..0000000 --- a/bin/japaTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Assert } from '@japa/assert' - -declare module '@japa/runner' { - interface TestContext { - assert: Assert - } -} diff --git a/bin/test.ts b/bin/test.ts index 5aba7ce..3ee1fcd 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,7 +1,7 @@ import { assert } from '@japa/assert' -import { specReporter } from '@japa/spec-reporter' -import { runFailedTests } from '@japa/run-failed-tests' -import { processCliArgs, configure, run } from '@japa/runner' +import { expectTypeOf } from '@japa/expect-type' +import { fileSystem } from '@japa/file-system' +import { processCLIArgs, configure, run } from '@japa/runner' /* |-------------------------------------------------------------------------- @@ -16,14 +16,10 @@ import { processCliArgs, configure, run } from '@japa/runner' | | Please consult japa.dev/runner-config for the config docs. */ +processCLIArgs(process.argv.slice(2)) configure({ - ...processCliArgs(process.argv.slice(2)), - ...{ - files: ['test/**/*.spec.ts'], - plugins: [assert(), runFailedTests()], - reporters: [specReporter()], - importer: (filePath: string) => import(filePath), - }, + files: ['tests/**/*.spec.ts'], + plugins: [assert(), expectTypeOf(), fileSystem()], }) /* diff --git a/configure.ts b/configure.ts new file mode 100644 index 0000000..3854d21 --- /dev/null +++ b/configure.ts @@ -0,0 +1,109 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type Configure from '@adonisjs/core/commands/configure' +import { stubsRoot } from './stubs/main.js' + +/** + * List of available providers + */ +const AVAILABLE_PROVIDERS = [ + 'discord', + 'facebook', + 'github', + 'google', + 'linkedin', + 'spotify', + 'twitter', +] + +/** + * Configures the package + */ +export async function configure(command: Configure) { + /** + * Read providers from the CLI flags + */ + let selectedProviders: string[] | string | undefined = command.parsedFlags.providers + + /** + * Otherwise force prompt for selection + */ + if (!selectedProviders) { + selectedProviders = await command.prompt.multiple( + 'Select the social auth providers you plan to use', + AVAILABLE_PROVIDERS, + { + validate(value) { + return !value || !value.length + ? 'Select a social provider to configure the package' + : true + }, + } + ) + } + + /** + * Cast CLI string value to an array + */ + let providers = ( + typeof selectedProviders === 'string' ? [selectedProviders] : selectedProviders + ) as string[] + + /** + * Validate CLI selection to contain known providers + */ + const unknownProvider = providers.find((provider) => !AVAILABLE_PROVIDERS.includes(provider)) + if (unknownProvider) { + command.exitCode = 1 + command.logger.error(`Invalid social provider "${unknownProvider}"`) + return + } + + const codemods = await command.createCodemods() + + /** + * Publish config file + */ + await codemods.makeUsingStub(stubsRoot, 'config/ally.stub', { + providers: providers.map((provider) => { + return { provider, envPrefix: provider.toUpperCase() } + }), + }) + + /** + * Publish provider + */ + await codemods.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/ally/ally_provider') + }) + + /** + * Define env variables for the selected providers + */ + await codemods.defineEnvVariables( + providers.reduce>((result, provider) => { + result[`${provider.toUpperCase()}_CLIENT_ID`] = '' + result[`${provider.toUpperCase()}_CLIENT_SECRET`] = '' + return result + }, {}) + ) + + /** + * Define env variables validation for the selected providers + */ + await codemods.defineEnvValidations({ + variables: providers.reduce>((result, provider) => { + result[`${provider.toUpperCase()}_CLIENT_ID`] = 'Env.schema.string()' + result[`${provider.toUpperCase()}_CLIENT_SECRET`] = 'Env.schema.string()' + return result + }, {}), + leadingComment: 'Variables for configuring ally package', + }) +} diff --git a/examples/app.ts b/examples/app.ts index 0a2f450..e1a05a2 100644 --- a/examples/app.ts +++ b/examples/app.ts @@ -1,18 +1,49 @@ -import { join } from 'path' -import { createServer } from 'http' -import { Application } from '@adonisjs/core/build/standalone' +import { Env } from '@adonisjs/core/env' +import { IgnitorFactory } from '@adonisjs/core/factories' + +const APP_ROOT = new URL('./', import.meta.url) +const IMPORTER = (filePath: string) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, APP_ROOT).href) + } + return import(filePath) +} + +await Env.create(new URL('../', APP_ROOT), {}) +const allyConfig = await import('./config/ally.js') async function run() { - const app = new Application(join(__dirname, '../'), 'web') - await app.setup() - await app.registerProviders() - await app.bootProviders() - await app.requirePreloads() + const ignitor = new IgnitorFactory() + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + ally: allyConfig.default, + }, + }) + .merge({ + rcFileContents: { + providers: ['../providers/ally_provider.js'], + preloads: [ + './discord.js', + './github.js', + './twitter.js', + './google.js', + './linkedin.js', + './facebook.js', + './spotify.js', + ], + }, + }) + .create(APP_ROOT, { + importer: IMPORTER, + }) - const server = app.container.use('Adonis/Core/Server') - server.optimize() - const port = process.env.PORT ? parseInt(process.env.PORT) : 3000 - createServer(server.handle.bind(server)).listen(port) + await ignitor.httpServer().start() } run() + .catch(console.error) + .then(() => { + console.log('ready') + }) diff --git a/examples/bitbucket.ts b/examples/bitbucket.ts index 5aa39a9..d936114 100644 --- a/examples/bitbucket.ts +++ b/examples/bitbucket.ts @@ -1,7 +1,7 @@ // /* // * @adonisjs/ally // * -// * (c) Harminder Virk +// * (c) AdonisJS // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. diff --git a/examples/config/ally.ts b/examples/config/ally.ts index 79893fa..4cc09ee 100644 --- a/examples/config/ally.ts +++ b/examples/config/ally.ts @@ -1,49 +1,44 @@ -import Env from '@ioc:Adonis/Core/Env' -import { AllyConfig } from '@ioc:Adonis/Addons/Ally' +import { defineConfig, services } from '../../index.js' -const allyConfig: AllyConfig = { - discord: { - driver: 'discord', - clientId: Env.get('DISCORD_CLIENT_ID'), - clientSecret: Env.get('DISCORD_CLIENT_SECRET'), - callbackUrl: `http://localhost:${Env.get('PORT')}/discord/callback`, - }, - google: { - driver: 'google', - clientId: Env.get('GOOGLE_CLIENT_ID'), - clientSecret: Env.get('GOOGLE_CLIENT_SECRET'), - callbackUrl: `http://localhost:${Env.get('PORT')}/google/callback`, - }, - github: { - driver: 'github', - clientId: Env.get('GITHUB_CLIENT_ID'), - clientSecret: Env.get('GITHUB_CLIENT_SECRET'), - callbackUrl: `http://localhost:${Env.get('PORT')}/github/callback`, - }, - linkedin: { - driver: 'linkedin', - clientId: Env.get('LINKEDIN_CLIENT_ID'), - clientSecret: Env.get('LINKEDIN_CLIENT_SECRET'), - callbackUrl: `http://localhost:${Env.get('PORT')}/linkedin/callback`, - }, - twitter: { - driver: 'twitter', - clientId: Env.get('TWITTER_API_KEY'), - clientSecret: Env.get('TWITTER_APP_SECRET'), - callbackUrl: `http://localhost:${Env.get('PORT')}/twitter/callback`, - }, - facebook: { - driver: 'facebook', - clientId: Env.get('FACEBOOK_CLIENT_ID'), - clientSecret: Env.get('FACEBOOK_CLIENT_SECRET'), - callbackUrl: `http://localhost:${Env.get('PORT')}/facebook/callback`, - }, - spotify: { - driver: 'spotify', - clientId: Env.get('SPOTIFY_CLIENT_ID'), - clientSecret: Env.get('SPOTIFY_CLIENT_SECRET'), - callbackUrl: `http://localhost:${Env.get('PORT')}/spotify/callback`, - }, -} +const allyConfig = defineConfig({ + discord: services.discord({ + clientId: process.env.DISCORD_CLIENT_ID!, + clientSecret: process.env.DISCORD_CLIENT_SECRET!, + callbackUrl: `http://localhost:${process.env.PORT}/discord/callback`, + }), + google: services.google({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + callbackUrl: `http://localhost:${process.env.PORT}/google/callback`, + }), + github: services.github({ + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + callbackUrl: `http://localhost:${process.env.PORT}/github/callback`, + }), + linkedin: services.linkedin({ + clientId: process.env.LINKEDIN_CLIENT_ID!, + clientSecret: process.env.LINKEDIN_CLIENT_SECRET!, + callbackUrl: `http://localhost:${process.env.PORT}/linkedin/callback`, + }), + twitter: services.twitter({ + clientId: process.env.TWITTER_API_KEY!, + clientSecret: process.env.TWITTER_APP_SECRET!, + callbackUrl: `http://localhost:${process.env.PORT}/twitter/callback`, + }), + facebook: services.facebook({ + clientId: process.env.FACEBOOK_CLIENT_ID!, + clientSecret: process.env.FACEBOOK_CLIENT_SECRET!, + callbackUrl: `http://localhost:${process.env.PORT}/facebook/callback`, + }), + spotify: services.spotify({ + clientId: process.env.SPOTIFY_CLIENT_ID!, + clientSecret: process.env.SPOTIFY_CLIENT_SECRET!, + callbackUrl: `http://localhost:${process.env.PORT}/spotify/callback`, + }), +}) +declare module '@adonisjs/ally/types' { + interface SocialProviders extends InferSocialProviders {} +} export default allyConfig diff --git a/examples/config/app.ts b/examples/config/app.ts deleted file mode 100644 index 6d1bb98..0000000 --- a/examples/config/app.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const appKey = 'averylongrandomsecretkey' -export const http = { - trustProxy: () => true, - cookie: {}, -} - -export const logger = { - enabled: true, - level: 'trace', -} diff --git a/examples/discord.ts b/examples/discord.ts index 2beac37..c74794c 100644 --- a/examples/discord.ts +++ b/examples/discord.ts @@ -1,25 +1,25 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import Route from '@ioc:Adonis/Core/Route' +import router from '@adonisjs/core/services/router' -Route.get('discord', async ({ response }) => { +router.get('discord', async ({ response }) => { return response.send(' Login with Discord') }) -Route.get('/discord/redirect', async ({ ally }) => { +router.get('/discord/redirect', async ({ ally }) => { return ally.use('discord').redirect((request) => { request.scopes(['identify', 'guilds']) }) }) -Route.get('/discord/callback', async ({ ally }) => { +router.get('/discord/callback', async ({ ally }) => { try { const discord = ally.use('discord') if (discord.accessDenied()) { diff --git a/examples/facebook.ts b/examples/facebook.ts index 97182c6..3d56e57 100644 --- a/examples/facebook.ts +++ b/examples/facebook.ts @@ -1,25 +1,25 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import Route from '@ioc:Adonis/Core/Route' +import router from '@adonisjs/core/services/router' -Route.get('facebook', async ({ response }) => { +router.get('facebook', async ({ response }) => { return response.send(' Login with facebook ') }) -Route.get('/facebook/redirect', async ({ ally }) => { +router.get('/facebook/redirect', async ({ ally }) => { return ally.use('facebook').redirect((request) => { request.scopes(['email']) }) }) -Route.get('/facebook/callback', async ({ ally }) => { +router.get('/facebook/callback', async ({ ally }) => { try { const facebook = ally.use('facebook') if (facebook.accessDenied()) { diff --git a/examples/github.ts b/examples/github.ts index 7c48d94..58c6aa9 100644 --- a/examples/github.ts +++ b/examples/github.ts @@ -1,25 +1,25 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import Route from '@ioc:Adonis/Core/Route' +import router from '@adonisjs/core/services/router' -Route.get('github', async ({ response }) => { +router.get('github', async ({ response }) => { return response.send(' Login with Github ') }) -Route.get('/github/redirect', async ({ ally }) => { +router.get('/github/redirect', async ({ ally }) => { return ally.use('github').redirect((request) => { request.scopes(['read:user']) }) }) -Route.get('/github/callback', async ({ ally }) => { +router.get('/github/callback', async ({ ally }) => { try { const gh = ally.use('github') if (gh.accessDenied()) { @@ -37,7 +37,7 @@ Route.get('/github/callback', async ({ ally }) => { const user = await gh.user() return user } catch (error) { - console.log({ error: error.response }) + console.log({ error: error.cause }) throw error } }) diff --git a/examples/gitlab.ts b/examples/gitlab.ts index 59785a0..5fa36c5 100644 --- a/examples/gitlab.ts +++ b/examples/gitlab.ts @@ -1,7 +1,7 @@ // /* // * @adonisjs/ally // * -// * (c) Harminder Virk +// * (c) AdonisJS // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. diff --git a/examples/google.ts b/examples/google.ts index ba7aac0..84953dd 100644 --- a/examples/google.ts +++ b/examples/google.ts @@ -1,23 +1,23 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import Route from '@ioc:Adonis/Core/Route' +import router from '@adonisjs/core/services/router' -Route.get('google', async ({ response }) => { +router.get('google', async ({ response }) => { return response.send(' Login with Google ') }) -Route.get('/google/redirect', async ({ ally }) => { +router.get('/google/redirect', async ({ ally }) => { return ally.use('google').redirect() }) -Route.get('/google/callback', async ({ ally }) => { +router.get('/google/callback', async ({ ally }) => { try { const google = ally.use('google') if (google.accessDenied()) { diff --git a/examples/linkedin.ts b/examples/linkedin.ts index afd5a65..1f80b84 100644 --- a/examples/linkedin.ts +++ b/examples/linkedin.ts @@ -1,23 +1,23 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import Route from '@ioc:Adonis/Core/Route' +import router from '@adonisjs/core/services/router' -Route.get('linkedin', async ({ response }) => { +router.get('linkedin', async ({ response }) => { return response.send(' Login with linkedin ') }) -Route.get('/linkedin/redirect', async ({ ally }) => { +router.get('/linkedin/redirect', async ({ ally }) => { return ally.use('linkedin').redirect() }) -Route.get('/linkedin/callback', async ({ ally }) => { +router.get('/linkedin/callback', async ({ ally }) => { try { const linkedin = ally.use('linkedin') if (linkedin.accessDenied()) { diff --git a/examples/microsoft.ts b/examples/microsoft.ts index 55653e5..b52c79c 100644 --- a/examples/microsoft.ts +++ b/examples/microsoft.ts @@ -1,7 +1,7 @@ // /* // * @adonisjs/ally // * -// * (c) Harminder Virk +// * (c) AdonisJS // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. diff --git a/examples/spotify.ts b/examples/spotify.ts index b76749b..39a6fe6 100644 --- a/examples/spotify.ts +++ b/examples/spotify.ts @@ -1,16 +1,25 @@ -import Route from '@ioc:Adonis/Core/Route' +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ -Route.get('spotify', async ({ response }) => { +import router from '@adonisjs/core/services/router' + +router.get('spotify', async ({ response }) => { return response.send(' Login with spotify ') }) -Route.get('/spotify/redirect', async ({ ally }) => { +router.get('/spotify/redirect', async ({ ally }) => { return ally.use('spotify').redirect((request) => { request.scopes(['user-read-email']) }) }) -Route.get('/spotify/callback', async ({ ally }) => { +router.get('/spotify/callback', async ({ ally }) => { try { const spotify = ally.use('spotify') if (spotify.accessDenied()) { diff --git a/examples/twitter.ts b/examples/twitter.ts index 8ed76c1..6cbfcec 100644 --- a/examples/twitter.ts +++ b/examples/twitter.ts @@ -1,23 +1,23 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import Route from '@ioc:Adonis/Core/Route' +import router from '@adonisjs/core/services/router' -Route.get('twitter', async ({ response }) => { +router.get('twitter', async ({ response }) => { return response.send(' Login with twitter ') }) -Route.get('/twitter/redirect', async ({ ally }) => { +router.get('/twitter/redirect', async ({ ally }) => { return ally.use('twitter').redirect() }) -Route.get('/twitter/callback', async ({ ally, request }) => { +router.get('/twitter/callback', async ({ ally, request }) => { console.log(request.cookiesList()) try { diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..af664d4 --- /dev/null +++ b/index.ts @@ -0,0 +1,20 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { HttpClient as ApiRequest } from '@poppinss/oauth-client' + +export * as errors from './src/errors.js' +export { configure } from './configure.js' +export { stubsRoot } from './stubs/main.js' +export { AllyManager } from './src/ally_manager.js' +export { defineConfig, services } from './src/define_config.js' + +export { RedirectRequest } from './src/redirect_request.js' +export { Oauth1Driver } from './src/abstract_drivers/oauth1.js' +export { Oauth2Driver } from './src/abstract_drivers/oauth2.js' diff --git a/instructions.ts b/instructions.ts deleted file mode 100644 index be9d066..0000000 --- a/instructions.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import * as sinkStatic from '@adonisjs/sink' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -type InstructionsState = { - providers: { - github: boolean - google: boolean - twitter: boolean - discord: boolean - linkedin: boolean - facebook: boolean - spotify: boolean - } - envVars: typeof ENV_VARS -} - -/** - * Base path to config stub partials - */ -const CONFIG_PARTIALS_BASE = './config/partials' - -/** - * Prompt choices for the provider selection - */ -const PROVIDER_PROMPT_CHOICES = [ - { - name: 'github' as const, - message: 'Github', - }, - { - name: 'google' as const, - message: 'Google', - }, - { - name: 'twitter' as const, - message: 'Twitter', - }, - { - name: 'discord' as const, - message: 'Discord', - }, - { - name: 'linkedin' as const, - message: 'LinkedIn', - }, - { - name: 'facebook' as const, - message: 'Facebook', - }, - { - name: 'spotify' as const, - message: 'Spotify', - }, -] - -/** - * Environment variables for available providers - */ -const ENV_VARS = { - github: { - clientId: 'GITHUB_CLIENT_ID', - clientSecret: 'GITHUB_CLIENT_SECRET', - }, - google: { - clientId: 'GOOGLE_CLIENT_ID', - clientSecret: 'GOOGLE_CLIENT_SECRET', - }, - twitter: { - clientId: 'TWITTER_CLIENT_ID', - clientSecret: 'TWITTER_CLIENT_SECRET', - }, - discord: { - clientId: 'DISCORD_CLIENT_ID', - clientSecret: 'DISCORD_CLIENT_SECRET', - }, - linkedin: { - clientId: 'LINKEDIN_CLIENT_ID', - clientSecret: 'LINKEDIN_CLIENT_SECRET', - }, - facebook: { - clientId: 'FACEBOOK_CLIENT_ID', - clientSecret: 'FACEBOOK_CLIENT_SECRET', - }, - spotify: { - clientId: 'SPOTIFY_CLIENT_ID', - clientSecret: 'SPOTIFY_CLIENT_SECRET', - }, -} - -/** - * Returns absolute path to the stub relative from the templates - * directory - */ -function getStub(...relativePaths: string[]) { - return join(__dirname, 'templates', ...relativePaths) -} - -/** - * Creates the contract file - */ -function makeContract( - projectRoot: string, - app: ApplicationContract, - sink: typeof sinkStatic, - state: InstructionsState -) { - const contractsDirectory = app.directoriesMap.get('contracts') || 'contracts' - const contractPath = join(contractsDirectory, 'ally.ts') - - const template = new sink.files.MustacheFile( - projectRoot, - contractPath, - getStub('contracts/ally.txt') - ) - template.overwrite = true - - template.apply(state).commit() - sink.logger.action('create').succeeded(contractPath) -} - -/** - * Makes the auth config file - */ -function makeConfig( - projectRoot: string, - app: ApplicationContract, - sink: typeof sinkStatic, - state: InstructionsState -) { - const configDirectory = app.directoriesMap.get('config') || 'config' - const configPath = join(configDirectory, 'ally.ts') - - const template = new sink.files.MustacheFile(projectRoot, configPath, getStub('config/ally.txt')) - template.overwrite = true - - /** - * Compute partials from selected providers - */ - const partials: any = {} - Object.keys(state.providers).forEach((provider) => { - if (state.providers[provider] === true) { - partials[`${provider}_provider`] = getStub(CONFIG_PARTIALS_BASE, `${provider}.txt`) - } - }) - - template.apply(state).partials(partials).commit() - sink.logger.action('create').succeeded(configPath) -} - -/** - * Define environment variables based upon user selection - */ -function defineEnvVars(projectRoot: string, sink: typeof sinkStatic, state: InstructionsState) { - const env = new sink.files.EnvFile(projectRoot) - - Object.keys(state.providers).forEach((provider) => { - if (state.providers[provider] === true) { - env.set(state.envVars[provider].clientId, 'clientId') - env.set(state.envVars[provider].clientSecret, 'clientSecret') - } else { - env.unset(state.envVars[provider].clientId) - env.unset(state.envVars[provider].clientSecret) - } - }) - - env.commit() - sink.logger.action('update').succeeded('.env,.env.example') -} - -/** - * Prompts user to select the provider - */ -async function getProvider(sink: typeof sinkStatic) { - return sink - .getPrompt() - .multiple('Select social providers you are planning to use', PROVIDER_PROMPT_CHOICES, { - validate(choice) { - return choice && choice.length ? true : 'Select at least one provider' - }, - }) -} - -/** - * Instructions to be executed when setting up the package. - */ -export default async function instructions( - projectRoot: string, - app: ApplicationContract, - sink: typeof sinkStatic -) { - const state: InstructionsState = { - providers: { - github: false, - google: false, - twitter: false, - discord: false, - linkedin: false, - facebook: false, - spotify: false, - }, - envVars: ENV_VARS, - } - - const selectedProviders = await getProvider(sink) - state.providers.discord = selectedProviders.includes('discord') - state.providers.github = selectedProviders.includes('github') - state.providers.google = selectedProviders.includes('google') - state.providers.twitter = selectedProviders.includes('twitter') - state.providers.linkedin = selectedProviders.includes('linkedin') - state.providers.facebook = selectedProviders.includes('facebook') - state.providers.spotify = selectedProviders.includes('spotify') - - /** - * Make contract file - */ - makeContract(projectRoot, app, sink, state) - - /** - * Make config file - */ - makeConfig(projectRoot, app, sink, state) - - /** - * Define env vars - */ - defineEnvVars(projectRoot, sink, state) -} diff --git a/package.json b/package.json index 4785087..227b625 100644 --- a/package.json +++ b/package.json @@ -1,154 +1,135 @@ { - "name": "@adonisjs/ally", - "version": "4.1.5", - "description": "Social authentication provider for AdonisJS", - "types": "./build/adonis-typings/index.d.ts", - "main": "build/providers/AllyProvider.js", - "files": [ - "build/adonis-typings", - "build/providers", - "build/templates", - "build/src", - "build/instructions.js", - "build/instructions.md", - "build/standalone.d.ts", - "build/standalone.js" - ], - "scripts": { - "mrm": "mrm --preset=@adonisjs/mrm-preset", - "pretest": "npm run lint", - "test": "node -r @adonisjs/require-ts/build/register bin/test.ts", - "clean": "del-cli build", - "start": "node -r @adonisjs/assembler/build/register examples/app.ts", - "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", - "compile": "npm run lint && npm run clean && tsc", - "build": "npm run compile && npm run copyfiles", - "prepublishOnly": "npm run build", - "lint": "eslint . --ext=.ts", - "format": "prettier --write .", - "commit": "git-cz", - "release": "np --message=\"chore(release): %s\"", - "version": "npm run build", - "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/ally" - }, - "keywords": [ - "adonis", - "adonisjs", - "social-auth", - "oauth" - ], - "author": "adonisjs,virk", - "license": "MIT", - "devDependencies": { - "@adonisjs/assembler": "^5.9.3", - "@adonisjs/core": "^5.8.7", - "@adonisjs/mrm-preset": "^5.0.3", - "@adonisjs/require-ts": "^2.0.13", - "@japa/assert": "^1.3.6", - "@japa/run-failed-tests": "^1.1.0", - "@japa/runner": "^2.2.1", - "@japa/spec-reporter": "^1.3.1", - "@poppinss/dev-utils": "^2.0.3", - "@types/node": "^18.8.4", - "commitizen": "^4.2.5", - "copyfiles": "^2.4.1", - "cz-conventional-changelog": "^3.3.0", - "del-cli": "^5.0.0", - "dotenv": "^16.0.3", - "eslint": "^8.25.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-adonis": "^2.1.1", - "eslint-plugin-prettier": "^4.2.1", - "github-label-sync": "^2.2.0", - "husky": "^8.0.1", - "mrm": "^4.1.6", - "nock": "^13.2.9", - "np": "^7.6.2", - "prettier": "^2.7.1", - "typescript": "^4.8.4" - }, - "husky": { - "hooks": { - "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" - } - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, - "np": { - "contents": ".", - "anyBranch": false - }, - "dependencies": { - "@poppinss/oauth-client": "^4.0.2", - "@poppinss/utils": "^5.0.0" - }, - "peerDependencies": { - "@adonisjs/core": "^5.8.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/adonisjs/adonis-ally.git" - }, - "bugs": { - "url": "https://github.com/adonisjs/adonis-ally/issues" - }, - "homepage": "https://github.com/adonisjs/adonis-ally#readme", - "publishConfig": { - "tag": "latest", - "access": "public" - }, - "adonisjs": { - "instructions": "./build/instructions.js", - "instructionsMd": "./build/instructions.md", - "types": "@adonisjs/ally", - "providers": [ - "@adonisjs/ally" - ] - }, - "mrmConfig": { - "core": true, - "license": "MIT", - "services": [ - "github-actions" - ], - "minNodeVersion": "14.15.4", - "probotApps": [ - "stale", - "lock" - ], - "runGhActionsOnWindows": false - }, - "eslintConfig": { - "extends": [ - "plugin:adonis/typescriptPackage", - "prettier" - ], - "plugins": [ - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ] - } - }, - "eslintIgnore": [ - "build" - ], - "prettier": { - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "useTabs": false, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 100 - } + "name": "@adonisjs/ally", + "version": "5.0.0-8", + "description": "Social authentication provider for AdonisJS", + "type": "module", + "main": "build/index.js", + "files": [ + "build", + "!build/bin", + "!build/examples", + "!build/tests" + ], + "exports": { + ".": "./build/index.js", + "./types": "./build/src/types.js", + "./ally_provider": "./build/providers/ally_provider.js" + }, + "engines": { + "node": ">=18.16.0" + }, + "scripts": { + "pretest": "npm run lint", + "test": "c8 npm run quick:test", + "clean": "del-cli build", + "typecheck": "tsc --noEmit", + "start": "node --loader=ts-node/esm examples/app.ts", + "copy:templates": "copyfiles --up=1 \"stubs/**/*.stub\" build", + "precompile": "npm run lint && npm run clean", + "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", + "postcompile": "npm run copy:templates", + "build": "npm run compile", + "prepublishOnly": "npm run build", + "lint": "eslint . --ext=.ts", + "format": "prettier --write .", + "release": "np", + "version": "npm run build", + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ally", + "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" + }, + "keywords": [ + "adonis", + "adonisjs", + "social-auth", + "oauth" + ], + "author": "adonisjs,virk", + "license": "MIT", + "devDependencies": { + "@adonisjs/assembler": "^7.0.0", + "@adonisjs/core": "^6.2.0", + "@adonisjs/eslint-config": "^1.2.1", + "@adonisjs/prettier-config": "^1.2.1", + "@adonisjs/tsconfig": "^1.2.1", + "@commitlint/cli": "^18.4.4", + "@commitlint/config-conventional": "^18.4.4", + "@japa/assert": "^2.1.0", + "@japa/expect-type": "^2.0.1", + "@japa/file-system": "^2.1.1", + "@japa/runner": "^3.1.1", + "@swc/core": "^1.3.102", + "@types/node": "^20.10.7", + "c8": "^9.0.0", + "copyfiles": "^2.4.1", + "del-cli": "^5.1.0", + "dotenv": "^16.3.1", + "eslint": "^8.56.0", + "github-label-sync": "^2.3.1", + "husky": "^8.0.3", + "nock": "^13.4.0", + "np": "^9.2.0", + "prettier": "^3.1.1", + "ts-node": "^10.9.2", + "tsup": "^8.0.1", + "typescript": "^5.3.3" + }, + "dependencies": { + "@poppinss/oauth-client": "^5.1.2", + "@poppinss/utils": "^6.7.0" + }, + "peerDependencies": { + "@adonisjs/core": "^6.2.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/adonisjs/adonis-ally.git" + }, + "bugs": { + "url": "https://github.com/adonisjs/adonis-ally/issues" + }, + "homepage": "https://github.com/adonisjs/adonis-ally#readme", + "publishConfig": { + "access": "public", + "tag": "next" + }, + "np": { + "message": "chore(release): %s", + "tag": "next", + "branch": "main", + "anyBranch": false + }, + "prettier": "@adonisjs/prettier-config", + "eslintConfig": { + "extends": "@adonisjs/eslint-config/package" + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "c8": { + "reporter": [ + "text", + "html" + ], + "exclude": [ + "tests/**", + "src/drivers/**", + "src/abstract_drivers/**", + "stubs/**" + ] + }, + "tsup": { + "entry": [ + "./index.ts", + "./src/types.ts", + "./providers/ally_provider.ts" + ], + "outDir": "./build", + "clean": true, + "format": "esm", + "dts": false, + "sourcemap": true, + "target": "esnext" + } } diff --git a/providers/AllyProvider.ts b/providers/AllyProvider.ts deleted file mode 100644 index 98eea92..0000000 --- a/providers/AllyProvider.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { AllyManager } from '../src/AllyManager' - -/** - * Ally provider to hook into an AdonisJS application - */ -export default class AllyProvider { - protected static needsApplication = true - constructor(private application: ApplicationContract) {} - - /** - * Register the binding - */ - public register() { - this.application.container.singleton('Adonis/Addons/Ally', (container) => { - const config = container.resolveBinding('Adonis/Core/Config').get('ally', {}) - return new AllyManager(this.application, config) - }) - } - - /** - * Stick an instance to the current HTTP request - */ - public boot() { - this.application.container.withBindings( - ['Adonis/Core/HttpContext', 'Adonis/Addons/Ally'], - (HttpContext, Ally) => { - HttpContext.getter( - 'ally', - function ally() { - return Ally.getAllyForRequest(this) - }, - true - ) - } - ) - } -} diff --git a/providers/ally_provider.ts b/providers/ally_provider.ts new file mode 100644 index 0000000..85df942 --- /dev/null +++ b/providers/ally_provider.ts @@ -0,0 +1,54 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { configProvider } from '@adonisjs/core' +import { HttpContext } from '@adonisjs/core/http' +import { RuntimeException } from '@poppinss/utils' +import type { ApplicationService } from '@adonisjs/core/types' + +import type { AllyService } from '../src/types.js' +import { AllyManager } from '../src/ally_manager.js' + +declare module '@adonisjs/core/http' { + export interface HttpContext { + ally: AllyService + } +} + +/** + * AllyProvider extends the HTTP context with the "ally" property + */ +export default class AllyProvider { + constructor(protected app: ApplicationService) {} + + async boot() { + const allyConfigProvider = this.app.config.get('ally') + + /** + * Resolve config from the provider + */ + const config = await configProvider.resolve(this.app, allyConfigProvider) + if (!config) { + throw new RuntimeException( + 'Invalid "config/ally.ts" file. Make sure you are using the "defineConfig" method' + ) + } + + /** + * Setup HTTPContext getter + */ + HttpContext.getter( + 'ally', + function (this: HttpContext) { + return new AllyManager(config, this) as unknown as AllyService + }, + true + ) + } +} diff --git a/src/Ally/index.ts b/src/Ally/index.ts deleted file mode 100644 index c67a2fb..0000000 --- a/src/Ally/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { - AllyContract, - Oauth1AccessToken, - Oauth2AccessToken, - AllyDriverContract, - AllyManagerContract, -} from '@ioc:Adonis/Addons/Ally' - -/** - * Ally allows constructing drivers for a given HTTP request. "use" is the only - * method we need. - */ -export class Ally implements AllyContract { - /** - * All drivers are cached during a given HTTP request - */ - private mappingsCache: Map< - string, - AllyDriverContract - > = new Map() - - constructor(private manager: AllyManagerContract, private ctx: HttpContextContract) {} - - /** - * Returns an instance of an ally driver. Driver instances are singleton during - * a given HTTP request - */ - public use(provider: string) { - if (!this.mappingsCache.has(provider)) { - this.mappingsCache.set(provider, this.manager.makeMapping(this.ctx, provider)) - } - - return this.mappingsCache.get(provider)! - } -} diff --git a/src/AllyManager/index.ts b/src/AllyManager/index.ts deleted file mode 100644 index 941ae80..0000000 --- a/src/AllyManager/index.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Exception } from '@poppinss/utils' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { - AllyContract, - SocialProviders, - GithubDriverConfig, - AllyManagerContract, - ExtendDriverCallback, - DiscordDriverConfig, - TwitterDriverConfig, - GoogleDriverConfig, - LinkedInDriverConfig, - FacebookDriverConfig, - SpotifyDriverConfig, -} from '@ioc:Adonis/Addons/Ally' - -import { Ally } from '../Ally' - -/** - * Manages the lifecycle of ally drivers and instantiates new instances - * for a given HTTP request - */ -export class AllyManager implements AllyManagerContract { - /** - * Extended set of ally drivers - */ - private extendedDrivers: Map = new Map() - - constructor(public application: ApplicationContract, private config: any) {} - - /** - * Returns the config for a given mapping from the config file - */ - protected getMappingConfig(name: string) { - const config = this.config[name] - - if (!config) { - throw new Exception( - `Missing config for social provider "${name}". Make sure it is defined inside the "config/ally" file` - ) - } - - if (!config.driver) { - throw new Exception(`Missing driver property on "${name}" provider config`) - } - - return config - } - - /** - * Make the discord driver - */ - protected makeDiscord(config: DiscordDriverConfig, ctx: HttpContextContract) { - const { DiscordDriver } = require('../Drivers/Discord') - return new DiscordDriver(ctx, config) - } - - /** - * Make the github driver - */ - protected makeGithub(config: GithubDriverConfig, ctx: HttpContextContract) { - const { GithubDriver } = require('../Drivers/Github') - return new GithubDriver(ctx, config) - } - - /** - * Make the twitter driver - */ - protected makeTwitter(config: TwitterDriverConfig, ctx: HttpContextContract) { - const { TwitterDriver } = require('../Drivers/Twitter') - return new TwitterDriver(ctx, config) - } - - /** - * Make the google driver - */ - protected makeGoogle(config: GoogleDriverConfig, ctx: HttpContextContract) { - const { GoogleDriver } = require('../Drivers/Google') - return new GoogleDriver(ctx, config) - } - - /** - * Make the linkedin driver - */ - protected makeLinkedIn(config: LinkedInDriverConfig, ctx: HttpContextContract) { - const { LinkedInDriver } = require('../Drivers/LinkedIn') - return new LinkedInDriver(ctx, config) - } - - /** - * Make the facebook driver - */ - protected makeFacebook(config: FacebookDriverConfig, ctx: HttpContextContract) { - const { FacebookDriver } = require('../Drivers/Facebook') - return new FacebookDriver(ctx, config) - } - - /** - * Make the spotify driver - */ - - protected makeSpotify(config: SpotifyDriverConfig, ctx: HttpContextContract) { - const { SpotifyDriver } = require('../Drivers/Spotify') - return new SpotifyDriver(ctx, config) - } - - /** - * Makes an instance of the extended driver - */ - protected makeExtendedDriver(mapping: string, config: any, ctx: HttpContextContract) { - const extendedCallback = this.extendedDrivers.get(config.driver) - if (typeof extendedCallback === 'function') { - return extendedCallback(this, mapping, config, ctx) - } - - throw new Exception(`Unknown ally driver "${config.driver}"`) - } - - /** - * Returns an instance of a mapping - */ - protected makeMappingInstance(mapping: string, ctx: HttpContextContract) { - const config = this.getMappingConfig(mapping) - switch (config.driver) { - case 'discord': - return this.makeDiscord(config, ctx) - case 'github': - return this.makeGithub(config, ctx) - case 'twitter': - return this.makeTwitter(config, ctx) - case 'google': - return this.makeGoogle(config, ctx) - case 'linkedin': - return this.makeLinkedIn(config, ctx) - case 'facebook': - return this.makeFacebook(config, ctx) - case 'spotify': - return this.makeSpotify(config, ctx) - default: - return this.makeExtendedDriver(mapping, config, ctx) - } - } - - /** - * Makes an instance of a given mapping - */ - public makeMapping(ctx: HttpContextContract, mapping: keyof SocialProviders) { - return this.makeMappingInstance(mapping, ctx) - } - - /** - * Returns an instance of ally, which can be later used to - * get instances of social providers for a given request - */ - public getAllyForRequest(ctx: HttpContextContract): AllyContract { - return new Ally(this, ctx) - } - - /** - * Add a new custom ally driver - */ - public extend(driverName: string, callback: ExtendDriverCallback): void { - if (typeof callback !== 'function') { - throw new Exception('"Ally.extend" expects callback to be a function') - } - - this.extendedDrivers.set(driverName, callback) - } -} diff --git a/src/Config/index.ts b/src/Config/index.ts deleted file mode 100644 index d441a65..0000000 --- a/src/Config/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export const twitter = { - REQUEST_TOKEN_URL: 'https://api.twitter.com/oauth/request_token', - AUTHORIZE_URL: 'https://api.twitter.com/oauth/authenticate', - ACCESS_TOKEN_URL: 'https://api.twitter.com/oauth/access_token', -} - -export const github = { - AUTHORIZE_URL: 'https://github.com/login/oauth/authorize', - ACCESS_TOKEN_URL: 'https://github.com/login/oauth/access_token', - USER_INFO_URL: 'https://api.github.com/user', - USER_EMAIL_URL: 'https://api.github.com/user/emails', -} - -export const google = { - AUTHORIZE_URL: 'https://accounts.google.com/o/oauth2/v2/auth', - ACCESS_TOKEN_URL: 'https://oauth2.googleapis.com/token', - USER_INFO_URL: 'https://www.googleapis.com/oauth2/v3/userinfo', -} - -export const gitlab = { - AUTHORIZE_URL: 'https://gitlab.com/oauth/authorize', - ACCESS_TOKEN_URL: 'https://gitlab.com/oauth/token', -} - -export const linkedin = { - AUTHORIZE_URL: 'https://www.linkedin.com/oauth/v2/authorization', - ACCESS_TOKEN_URL: 'https://www.linkedin.com/oauth/v2/accessToken', -} - -export const patreon = { - AUTHORIZE_URL: 'https://www.patreon.com/oauth2/authorize', - ACCESS_TOKEN_URL: 'https://www.patreon.com/api/oauth2/token', -} - -export const discord = { - AUTHORIZE_URL: 'https://discord.com/api/oauth2/authorize', - ACCESS_TOKEN_URL: 'https://discord.com/api/oauth2/token', - USER_INFO_URL: 'https://discord.com/api/users/@me', -} - -export const microsoft = { - AUTHORIZE_URL: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize', - ACCESS_TOKEN_URL: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token', -} - -export const bitbucket = { - AUTHORIZE_URL: 'https://bitbucket.org/site/oauth2/authorize', - ACCESS_TOKEN_URL: 'https://bitbucket.org/site/oauth2/access_token', -} - -export const facebook = { - AUTHORIZE_URL: 'https://www.facebook.com/v10.0/dialog/oauth', - ACCESS_TOKEN_URL: 'https://graph.facebook.com/v10.0/dialog/oauth/access_token', -} - -export const spotify = { - AUTHORIZE_URL: 'https://accounts.spotify.com/authorize', - ACCESS_TOKEN_URL: 'https://accounts.spotify.com/api/token', - USER_INFO_URL: 'https://api.spotify.com/v1/me', -} diff --git a/src/Exceptions/index.ts b/src/Exceptions/index.ts deleted file mode 100644 index 2d75ecb..0000000 --- a/src/Exceptions/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Exception } from '@poppinss/utils' - -export class OauthException extends Exception { - public static missingAuthorizationCode(paramName: string) { - return new this( - `Cannot request access token. Redirect request is missing the "${paramName}" param`, - 500, - 'E_OAUTH_MISSING_CODE' - ) - } - - /** - * Unable to verify state after redirect - */ - public static stateMisMatch() { - return new this('Unable to verify re-redirect state', 400, 'E_OAUTH_STATE_MISMATCH') - } -} diff --git a/src/RedirectRequest/index.ts b/src/RedirectRequest/index.ts deleted file mode 100644 index f416f00..0000000 --- a/src/RedirectRequest/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { UrlBuilder } from '@poppinss/oauth-client' -import { RedirectRequestContract, LiteralStringUnion } from '@ioc:Adonis/Addons/Ally' - -/** - * Redirect request with first class support for defining scopes. - */ -export class RedirectRequest - extends UrlBuilder - implements RedirectRequestContract -{ - private scopesTransformer: undefined | ((scopes: LiteralStringUnion[]) => string[]) - - constructor(baseUrl: string, private scopeParamName: string, private scopeSeparator: string) { - super(baseUrl) - } - - public transformScopes(callback: (scopes: LiteralStringUnion[]) => string[]): this { - this.scopesTransformer = callback - return this - } - - /** - * Define an array of scopes. - */ - public scopes(scopes: LiteralStringUnion[]): this { - if (typeof this.scopesTransformer === 'function') { - scopes = this.scopesTransformer(scopes) - } - - this.param(this.scopeParamName, scopes.join(this.scopeSeparator)) - return this - } - - /** - * Clear existing scopes - */ - public clearScopes(): this { - this.clearParam(this.scopeParamName) - return this - } - - /** - * Merge to existing scopes - */ - public mergeScopes(scopes: LiteralStringUnion[]): this { - if (typeof this.scopesTransformer === 'function') { - scopes = this.scopesTransformer(scopes) - } - - const mergedScopes = (this.params[this.scopeParamName] || []).concat(scopes) - this.scopes(mergedScopes) - - return this - } -} diff --git a/src/AbstractDrivers/Oauth1/index.ts b/src/abstract_drivers/oauth1.ts similarity index 81% rename from src/AbstractDrivers/Oauth1/index.ts rename to src/abstract_drivers/oauth1.ts index ea40f8e..148a199 100644 --- a/src/AbstractDrivers/Oauth1/index.ts +++ b/src/abstract_drivers/oauth1.ts @@ -1,17 +1,15 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { Exception } from '@poppinss/utils' -import { Oauth1Client } from '@poppinss/oauth-client' -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import type { HttpContext } from '@adonisjs/core/http' +import { Oauth1Client } from '@poppinss/oauth-client/oauth1' import { AllyUserContract, @@ -20,10 +18,10 @@ import { ApiRequestContract, AllyDriverContract, RedirectRequestContract, -} from '@ioc:Adonis/Addons/Ally' +} from '../types.js' -import { OauthException } from '../../Exceptions' -import { RedirectRequest } from '../../RedirectRequest' +import * as errors from '../errors.js' +import { RedirectRequest } from '../redirect_request.js' /** * Abstract implementation for an Oauth1 driver @@ -91,14 +89,12 @@ export abstract class Oauth1Driver void - ): Promise> + abstract user(callback?: (request: ApiRequestContract) => void): Promise> /** * Finds the user by access token */ - public abstract userFromTokenAndSecret( + abstract userFromTokenAndSecret( token: string, secret: string, callback?: (request: ApiRequestContract) => void @@ -107,12 +103,12 @@ export abstract class Oauth1Driver) => void ): Promise { - return this.getRedirectUrl(callback) + return this.getRedirectUrl(callback as any) } /** * Redirect user for authorization. */ - public async redirect( - callback?: (request: RedirectRequestContract) => void - ): Promise { + async redirect(callback?: (request: RedirectRequestContract) => void): Promise { const { token, secret } = await this.getRequestToken() /** * Storing token and secret inside cookies. We need them * later */ - this.persistToken(token) - this.persistSecret(secret) + this.#persistToken(token) + this.#persistSecret(secret) const url = await this.redirectUrl((request) => { request.param(this.oauthTokenParamName, token) @@ -229,21 +226,21 @@ export abstract class Oauth1Driver void): Promise { + async accessToken(callback?: (request: ApiRequestContract) => void): Promise { /** * We expect the user to handle errors before calling this method */ if (this.hasError()) { - throw OauthException.missingAuthorizationCode(this.oauthTokenVerifierName) + throw new errors.E_OAUTH_MISSING_CODE([this.oauthTokenVerifierName]) } /** @@ -286,7 +283,7 @@ export abstract class Oauth1Driver { + async userFromToken(): Promise { throw new Exception( '"userFromToken" is not available with Oauth1. Use "userFromTokenAndSecret" instead' ) diff --git a/src/AbstractDrivers/Oauth2/index.ts b/src/abstract_drivers/oauth2.ts similarity index 79% rename from src/AbstractDrivers/Oauth2/index.ts rename to src/abstract_drivers/oauth2.ts index ea22c64..90ee02f 100644 --- a/src/AbstractDrivers/Oauth2/index.ts +++ b/src/abstract_drivers/oauth2.ts @@ -1,17 +1,15 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { Exception } from '@poppinss/utils' -import { Oauth2Client } from '@poppinss/oauth-client' -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import type { HttpContext } from '@adonisjs/core/http' +import { Oauth2Client } from '@poppinss/oauth-client/oauth2' import { AllyUserContract, @@ -20,10 +18,10 @@ import { ApiRequestContract, AllyDriverContract, RedirectRequestContract, -} from '@ioc:Adonis/Addons/Ally' +} from '../types.js' -import { OauthException } from '../../Exceptions' -import { RedirectRequest } from '../../RedirectRequest' +import * as errors from '../errors.js' +import { RedirectRequest } from '../redirect_request.js' /** * Abstract implementation for an Oauth2 driver @@ -93,14 +91,12 @@ export abstract class Oauth2Driver void - ): Promise> + abstract user(callback?: (request: ApiRequestContract) => void): Promise> /** * Finds the user by access token */ - public abstract userFromToken( + abstract userFromToken( token: string, callback?: (request: ApiRequestContract) => void ): Promise> @@ -108,19 +104,22 @@ export abstract class Oauth2Driver) => void ): Promise { - const url = this.getRedirectUrl(callback) + const url = this.getRedirectUrl(callback as any) return url } /** * Redirect user for authorization. */ - public async redirect( - callback?: (request: RedirectRequestContract) => void - ): Promise { + async redirect(callback?: (request: RedirectRequestContract) => void): Promise { const url = await this.redirectUrl((request) => { - const state = this.persistState() + const state = this.#persistState() state && request.param(this.stateParamName, state) if (typeof callback === 'function') { @@ -208,7 +205,7 @@ export abstract class Oauth2Driver void): Promise { + async accessToken(callback?: (request: ApiRequestContract) => void): Promise { /** * We expect the user to handle errors before calling this method */ if (this.hasError()) { - throw OauthException.missingAuthorizationCode(this.codeParamName) + throw new errors.E_OAUTH_MISSING_CODE([this.codeParamName]) } /** @@ -269,7 +266,7 @@ export abstract class Oauth2Driver { + async userFromTokenAndSecret(): Promise { throw new Exception( '"userFromTokenAndSecret" is not applicable with Oauth2. Use "userFromToken" instead' ) diff --git a/src/ally_manager.ts b/src/ally_manager.ts new file mode 100644 index 0000000..f40b5c0 --- /dev/null +++ b/src/ally_manager.ts @@ -0,0 +1,55 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { RuntimeException } from '@poppinss/utils' +import type { HttpContext } from '@adonisjs/core/http' +import type { AllyDriverContract, AllyManagerDriverFactory } from './types.js' + +/** + * AllyManager is used to create instances of a social drivers during an + * HTTP request. The drivers are cached during the lifecycle of a request. + */ +export class AllyManager> { + /** + * Config with the list of social providers + */ + #config: KnownSocialProviders + #ctx: HttpContext + #driversCache: Map> = new Map() + + constructor(config: KnownSocialProviders, ctx: HttpContext) { + this.#ctx = ctx + this.#config = config + } + + /** + * Returns the driver instance of a social provider + */ + use( + provider: SocialProvider + ): ReturnType { + if (this.#driversCache.has(provider)) { + return this.#driversCache.get(provider) as ReturnType + } + + const driver = this.#config[provider] + if (!driver) { + throw new RuntimeException( + `Unknown ally provider "${String( + provider + )}". Make sure it is registered inside the config/ally.ts file` + ) + } + + const driverInstance = driver(this.#ctx) as ReturnType + this.#driversCache.set(provider, driverInstance) + + return driverInstance + } +} diff --git a/adonis-typings/index.ts b/src/debug.ts similarity index 50% rename from adonis-typings/index.ts rename to src/debug.ts index ff3289f..d6c47bf 100644 --- a/adonis-typings/index.ts +++ b/src/debug.ts @@ -1,12 +1,12 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// -/// -/// +import { debuglog } from 'node:util' + +export default debuglog('adonisjs:ally') diff --git a/src/define_config.ts b/src/define_config.ts new file mode 100644 index 0000000..7ae93cf --- /dev/null +++ b/src/define_config.ts @@ -0,0 +1,127 @@ +/* + * @adonisjs/ally + * + * (c) Ally + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { configProvider } from '@adonisjs/core' +import type { HttpContext } from '@adonisjs/core/http' +import type { ConfigProvider } from '@adonisjs/core/types' + +import type { GoogleDriver } from './drivers/google.js' +import type { GithubDriver } from './drivers/github.js' +import type { SpotifyDriver } from './drivers/spotify.js' +import type { TwitterDriver } from './drivers/twitter.js' +import type { DiscordDriver } from './drivers/discord.js' +import type { FacebookDriver } from './drivers/facebook.js' +import type { LinkedInDriver } from './drivers/linked_in.js' +import type { + GoogleDriverConfig, + GithubDriverConfig, + SpotifyDriverConfig, + DiscordDriverConfig, + TwitterDriverConfig, + LinkedInDriverConfig, + FacebookDriverConfig, + AllyManagerDriverFactory, +} from './types.js' + +/** + * Shape of config after it has been resolved from + * the config provider + */ +type ResolvedConfig< + KnownSocialProviders extends Record< + string, + AllyManagerDriverFactory | ConfigProvider + >, +> = { + [K in keyof KnownSocialProviders]: KnownSocialProviders[K] extends ConfigProvider + ? A + : KnownSocialProviders[K] +} + +/** + * Define config for the ally + */ +export function defineConfig< + KnownSocialProviders extends Record< + string, + AllyManagerDriverFactory | ConfigProvider + >, +>(config: KnownSocialProviders): ConfigProvider> { + return configProvider.create(async (app) => { + const serviceNames = Object.keys(config) + const services = {} as Record + + for (let serviceName of serviceNames) { + const service = config[serviceName] + if (typeof service === 'function') { + services[serviceName] = service + } else { + services[serviceName] = await service.resolver(app) + } + } + + return services as ResolvedConfig + }) +} + +/** + * Helpers to configure social auth services + */ +export const services: { + discord: (config: DiscordDriverConfig) => ConfigProvider<(ctx: HttpContext) => DiscordDriver> + facebook: (config: FacebookDriverConfig) => ConfigProvider<(ctx: HttpContext) => FacebookDriver> + github: (config: GithubDriverConfig) => ConfigProvider<(ctx: HttpContext) => GithubDriver> + google: (config: GoogleDriverConfig) => ConfigProvider<(ctx: HttpContext) => GoogleDriver> + linkedin: (config: LinkedInDriverConfig) => ConfigProvider<(ctx: HttpContext) => LinkedInDriver> + spotify: (config: SpotifyDriverConfig) => ConfigProvider<(ctx: HttpContext) => SpotifyDriver> + twitter: (config: TwitterDriverConfig) => ConfigProvider<(ctx: HttpContext) => TwitterDriver> +} = { + discord(config) { + return configProvider.create(async () => { + const { DiscordDriver } = await import('./drivers/discord.js') + return (ctx) => new DiscordDriver(ctx, config) + }) + }, + facebook(config) { + return configProvider.create(async () => { + const { FacebookDriver } = await import('./drivers/facebook.js') + return (ctx) => new FacebookDriver(ctx, config) + }) + }, + github(config) { + return configProvider.create(async () => { + const { GithubDriver } = await import('./drivers/github.js') + return (ctx) => new GithubDriver(ctx, config) + }) + }, + google(config) { + return configProvider.create(async () => { + const { GoogleDriver } = await import('./drivers/google.js') + return (ctx) => new GoogleDriver(ctx, config) + }) + }, + linkedin(config) { + return configProvider.create(async () => { + const { LinkedInDriver } = await import('./drivers/linked_in.js') + return (ctx) => new LinkedInDriver(ctx, config) + }) + }, + spotify(config) { + return configProvider.create(async () => { + const { SpotifyDriver } = await import('./drivers/spotify.js') + return (ctx) => new SpotifyDriver(ctx, config) + }) + }, + twitter(config) { + return configProvider.create(async () => { + const { TwitterDriver } = await import('./drivers/twitter.js') + return (ctx) => new TwitterDriver(ctx, config) + }) + }, +} diff --git a/src/Drivers/Discord/index.ts b/src/drivers/discord.ts similarity index 86% rename from src/Drivers/Discord/index.ts rename to src/drivers/discord.ts index 1fca1ee..2866d5b 100644 --- a/src/Drivers/Discord/index.ts +++ b/src/drivers/discord.ts @@ -1,30 +1,28 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { +import type { HttpContext } from '@adonisjs/core/http' +import type { HttpClient } from '@poppinss/oauth-client' + +import type { DiscordScopes, DiscordToken, ApiRequestContract, DiscordDriverConfig, - DiscordDriverContract, RedirectRequestContract, -} from '@ioc:Adonis/Addons/Ally' -import { Oauth2Driver } from '../../AbstractDrivers/Oauth2' +} from '../types.js' +import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * Discord driver to login user via Discord */ -export class DiscordDriver - extends Oauth2Driver - implements DiscordDriverContract -{ +export class DiscordDriver extends Oauth2Driver { protected accessTokenUrl = 'https://discord.com/api/oauth2/token' protected authorizeUrl = 'https://discord.com/api/oauth2/authorize' protected userInfoUrl = 'https://discord.com/api/users/@me' @@ -60,7 +58,10 @@ export class DiscordDriver */ protected scopesSeparator = ' ' - constructor(ctx: HttpContextContract, public config: DiscordDriverConfig) { + constructor( + ctx: HttpContext, + public config: DiscordDriverConfig + ) { super(ctx, config) /** @@ -114,7 +115,7 @@ export class DiscordDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') @@ -156,7 +157,7 @@ export class DiscordDriver /** * Find if the current error code is for access denied */ - public accessDenied(): boolean { + accessDenied(): boolean { const error = this.getError() if (!error) { return false @@ -168,7 +169,7 @@ export class DiscordDriver /** * Returns details for the authorized user */ - public async user(callback?: (request: ApiRequestContract) => void) { + async user(callback?: (request: ApiRequestContract) => void) { const token = await this.accessToken(callback) const user = await this.getUserInfo(token.token, callback) @@ -181,7 +182,7 @@ export class DiscordDriver /** * Finds the user by the access token */ - public async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { + async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { const user = await this.getUserInfo(token, callback) return { diff --git a/src/Drivers/Facebook/index.ts b/src/drivers/facebook.ts similarity index 86% rename from src/Drivers/Facebook/index.ts rename to src/drivers/facebook.ts index b02123f..bae4224 100644 --- a/src/Drivers/Facebook/index.ts +++ b/src/drivers/facebook.ts @@ -1,32 +1,29 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { +import type { HttpClient } from '@poppinss/oauth-client' +import type { HttpContext } from '@adonisjs/core/http' +import type { FacebookToken, FacebookScopes, LiteralStringUnion, ApiRequestContract, FacebookDriverConfig, FacebookProfileFields, - FacebookDriverContract, RedirectRequestContract, -} from '@ioc:Adonis/Addons/Ally' -import { Oauth2Driver } from '../../AbstractDrivers/Oauth2' +} from '../types.js' +import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * Facebook driver to login user via Facebook */ -export class FacebookDriver - extends Oauth2Driver - implements FacebookDriverContract -{ +export class FacebookDriver extends Oauth2Driver { protected accessTokenUrl = 'https://graph.facebook.com/v10.0/oauth/access_token' protected authorizeUrl = 'https://www.facebook.com/v10.0/dialog/oauth' protected userInfoUrl = 'https://graph.facebook.com/v10.0/me' @@ -75,7 +72,10 @@ export class FacebookDriver */ protected scopesSeparator = ' ' - constructor(ctx: HttpContextContract, public config: FacebookDriverConfig) { + constructor( + ctx: HttpContext, + public config: FacebookDriverConfig + ) { super(ctx, config) /** @@ -111,7 +111,7 @@ export class FacebookDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') @@ -157,7 +157,7 @@ export class FacebookDriver /** * Find if the current error code is for access denied */ - public accessDenied(): boolean { + accessDenied(): boolean { const error = this.getError() if (!error) { return false @@ -169,7 +169,7 @@ export class FacebookDriver /** * Returns details for the authorized user */ - public async user(callback?: (request: ApiRequestContract) => void) { + async user(callback?: (request: ApiRequestContract) => void) { const token = await this.accessToken(callback) const user = await this.getUserInfo(token.token, callback) @@ -182,7 +182,7 @@ export class FacebookDriver /** * Finds the user by the access token */ - public async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { + async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { const user = await this.getUserInfo(token, callback) return { diff --git a/src/Drivers/Github/index.ts b/src/drivers/github.ts similarity index 89% rename from src/Drivers/Github/index.ts rename to src/drivers/github.ts index 57aa6e3..4e5129b 100644 --- a/src/Drivers/Github/index.ts +++ b/src/drivers/github.ts @@ -1,31 +1,28 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { +import type { HttpContext } from '@adonisjs/core/http' +import type { HttpClient } from '@poppinss/oauth-client' +import type { GithubToken, GithubScopes, AllyUserContract, GithubDriverConfig, ApiRequestContract, - GithubDriverContract, RedirectRequestContract, -} from '@ioc:Adonis/Addons/Ally' -import { Oauth2Driver } from '../../AbstractDrivers/Oauth2' +} from '../types.js' +import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * Github driver to login user via Github */ -export class GithubDriver - extends Oauth2Driver - implements GithubDriverContract -{ +export class GithubDriver extends Oauth2Driver { protected accessTokenUrl = 'https://github.com/login/oauth/access_token' protected authorizeUrl = 'https://github.com/login/oauth/authorize' protected userInfoUrl = 'https://api.github.com/user' @@ -62,7 +59,10 @@ export class GithubDriver */ protected scopesSeparator = ' ' - constructor(ctx: HttpContextContract, public config: GithubDriverConfig) { + constructor( + ctx: HttpContext, + public config: GithubDriverConfig + ) { super(ctx, config) /** * Extremely important to call the following method to clear the @@ -116,7 +116,7 @@ export class GithubDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `token ${token}`) request.header('Accept', 'application/json') @@ -194,7 +194,7 @@ export class GithubDriver /** * Find if the current error code is for access denied */ - public accessDenied(): boolean { + accessDenied(): boolean { const error = this.getError() if (!error) { return false @@ -206,7 +206,7 @@ export class GithubDriver /** * Returns details for the authorized user */ - public async user(callback?: (request: ApiRequestContract) => void) { + async user(callback?: (request: ApiRequestContract) => void) { const token = await this.accessToken(callback) const user = await this.getUserInfo(token.token, callback) @@ -234,7 +234,7 @@ export class GithubDriver /** * Finds the user by the access token */ - public async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { + async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { const user = await this.getUserInfo(token, callback) /** diff --git a/src/Drivers/Google/index.ts b/src/drivers/google.ts similarity index 83% rename from src/Drivers/Google/index.ts rename to src/drivers/google.ts index df51926..608d8ca 100644 --- a/src/Drivers/Google/index.ts +++ b/src/drivers/google.ts @@ -1,22 +1,22 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { +import type { HttpContext } from '@adonisjs/core/http' +import type { HttpClient } from '@poppinss/oauth-client' +import type { GoogleToken, GoogleScopes, GoogleDriverConfig, ApiRequestContract, - GoogleDriverContract, RedirectRequestContract, -} from '@ioc:Adonis/Addons/Ally' -import { Oauth2Driver } from '../../AbstractDrivers/Oauth2' +} from '../types.js' +import { Oauth2Driver } from '../abstract_drivers/oauth2.js' const SCOPE_PREFIXES = { 'https://www.googleapis.com/auth': [ @@ -59,10 +59,7 @@ const SCOPE_PREFIXES = { /** * Google driver to login user via Google */ -export class GoogleDriver - extends Oauth2Driver - implements GoogleDriverContract -{ +export class GoogleDriver extends Oauth2Driver { protected accessTokenUrl = 'https://oauth2.googleapis.com/token' protected authorizeUrl = 'https://accounts.google.com/o/oauth2/v2/auth' protected userInfoUrl = 'https://www.googleapis.com/oauth2/v3/userinfo' @@ -98,7 +95,10 @@ export class GoogleDriver */ protected scopesSeparator = ' ' - constructor(ctx: HttpContextContract, public config: GoogleDriverConfig) { + constructor( + ctx: HttpContext, + public config: GoogleDriverConfig + ) { super(ctx, config) /** * Extremely important to call the following method to clear the @@ -143,7 +143,7 @@ export class GoogleDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') @@ -176,7 +176,7 @@ export class GoogleDriver /** * Find if the current error code is for access denied */ - public accessDenied(): boolean { + accessDenied(): boolean { const error = this.getError() if (!error) { return false @@ -188,7 +188,7 @@ export class GoogleDriver /** * Get access token */ - public async accessToken(callback?: (request: ApiRequestContract) => void): Promise { + async accessToken(callback?: (request: ApiRequestContract) => void): Promise { const token = await super.accessToken(callback) return { @@ -200,7 +200,7 @@ export class GoogleDriver /** * Returns details for the authorized user */ - public async user(callback?: (request: ApiRequestContract) => void) { + async user(callback?: (request: ApiRequestContract) => void) { const token = await this.accessToken(callback) const user = await this.getUserInfo(token.token, callback) @@ -213,7 +213,7 @@ export class GoogleDriver /** * Finds the user by the access token */ - public async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { + async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { const user = await this.getUserInfo(token, callback) return { @@ -225,9 +225,11 @@ export class GoogleDriver /** * Prefixes google scopes with the url */ - public buildScopes(scopes: string[]) { + buildScopes(scopes: string[]) { return scopes.map((name) => { - const prefix = Object.keys(SCOPE_PREFIXES).find((one) => SCOPE_PREFIXES[one].includes(name)) + const prefix = Object.keys(SCOPE_PREFIXES).find((one) => + SCOPE_PREFIXES[one as keyof typeof SCOPE_PREFIXES].includes(name) + ) return prefix ? `${prefix}/${name}` : name }) } diff --git a/src/Drivers/LinkedIn/index.ts b/src/drivers/linked_in.ts similarity index 87% rename from src/Drivers/LinkedIn/index.ts rename to src/drivers/linked_in.ts index 33a4317..59be455 100644 --- a/src/Drivers/LinkedIn/index.ts +++ b/src/drivers/linked_in.ts @@ -1,31 +1,28 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import { Exception } from '@poppinss/utils' -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { +import type { HttpContext } from '@adonisjs/core/http' +import type { HttpClient } from '@poppinss/oauth-client' +import type { LinkedInToken, LinkedInScopes, ApiRequestContract, LinkedInDriverConfig, - LinkedInDriverContract, RedirectRequestContract, -} from '@ioc:Adonis/Addons/Ally' -import { Oauth2Driver } from '../../AbstractDrivers/Oauth2' +} from '../types.js' +import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * LinkedIn driver to login user via LinkedIn */ -export class LinkedInDriver - extends Oauth2Driver - implements LinkedInDriverContract -{ +export class LinkedInDriver extends Oauth2Driver { protected accessTokenUrl = 'https://www.linkedin.com/oauth/v2/accessToken' protected authorizeUrl = 'https://www.linkedin.com/oauth/v2/authorization' protected userInfoUrl = 'https://api.linkedin.com/v2/me' @@ -62,7 +59,10 @@ export class LinkedInDriver */ protected scopesSeparator = ' ' - constructor(ctx: HttpContextContract, public config: LinkedInDriverConfig) { + constructor( + ctx: HttpContext, + public config: LinkedInDriverConfig + ) { super(ctx, config) /** * Extremely important to call the following method to clear the @@ -89,7 +89,7 @@ export class LinkedInDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') @@ -167,7 +167,7 @@ export class LinkedInDriver /** * Find if the current error code is for access denied */ - public accessDenied(): boolean { + accessDenied(): boolean { const error = this.getError() if (!error) { return false @@ -179,7 +179,7 @@ export class LinkedInDriver /** * Returns details for the authorized user */ - public async user(callback?: (request: ApiRequestContract) => void) { + async user(callback?: (request: ApiRequestContract) => void) { const token = await this.accessToken(callback) const user = await this.getUserInfo(token.token, callback) const email = await this.getUserEmail(token.token, callback) @@ -195,7 +195,7 @@ export class LinkedInDriver /** * Finds the user by the access token */ - public async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { + async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { const user = await this.getUserInfo(token, callback) const email = await this.getUserEmail(token, callback) diff --git a/src/Drivers/Spotify/index.ts b/src/drivers/spotify.ts similarity index 82% rename from src/Drivers/Spotify/index.ts rename to src/drivers/spotify.ts index 9081651..e1aec95 100644 --- a/src/Drivers/Spotify/index.ts +++ b/src/drivers/spotify.ts @@ -1,30 +1,27 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { +import type { HttpContext } from '@adonisjs/core/http' +import type { HttpClient } from '@poppinss/oauth-client' +import type { SpotifyScopes, SpotifyToken, ApiRequestContract, SpotifyDriverConfig, - SpotifyDriverContract, RedirectRequestContract, -} from '@ioc:Adonis/Addons/Ally' -import { Oauth2Driver } from '../../AbstractDrivers/Oauth2' +} from '../types.js' +import { Oauth2Driver } from '../abstract_drivers/oauth2.js' /** * Spotify driver to login user via Spotify */ -export class SpotifyDriver - extends Oauth2Driver - implements SpotifyDriverContract -{ +export class SpotifyDriver extends Oauth2Driver { protected accessTokenUrl = 'https://accounts.spotify.com/api/token' protected authorizeUrl = 'https://accounts.spotify.com/authorize' protected userInfoUrl = 'https://api.spotify.com/v1/me' @@ -60,7 +57,10 @@ export class SpotifyDriver */ protected scopesSeparator = ' ' - constructor(ctx: HttpContextContract, public config: SpotifyDriverConfig) { + constructor( + ctx: HttpContext, + public config: SpotifyDriverConfig + ) { super(ctx, config) /** @@ -93,7 +93,7 @@ export class SpotifyDriver /** * Returns the HTTP request with the authorization header set */ - protected getAuthenticatedRequest(url: string, token: string) { + protected getAuthenticatedRequest(url: string, token: string): HttpClient { const request = this.httpClient(url) request.header('Authorization', `Bearer ${token}`) request.header('Accept', 'application/json') @@ -127,7 +127,7 @@ export class SpotifyDriver /** * Find if the current error code is for access denied */ - public accessDenied(): boolean { + accessDenied(): boolean { const error = this.getError() if (!error) { return false @@ -139,7 +139,7 @@ export class SpotifyDriver /** * Returns details for the authorized user */ - public async user(callback?: (request: ApiRequestContract) => void) { + async user(callback?: (request: ApiRequestContract) => void) { const token = await this.accessToken(callback) const user = await this.getUserInfo(token.token, callback) @@ -152,7 +152,7 @@ export class SpotifyDriver /** * Finds the user by the access token */ - public async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { + async userFromToken(token: string, callback?: (request: ApiRequestContract) => void) { const user = await this.getUserInfo(token, callback) return { diff --git a/src/Drivers/Twitter/index.ts b/src/drivers/twitter.ts similarity index 85% rename from src/Drivers/Twitter/index.ts rename to src/drivers/twitter.ts index d27dd73..ab83fb3 100644 --- a/src/Drivers/Twitter/index.ts +++ b/src/drivers/twitter.ts @@ -1,29 +1,25 @@ /* * @adonisjs/ally * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { Oauth1Driver } from '../../AbstractDrivers/Oauth1' +import type { HttpContext } from '@adonisjs/core/http' import { TwitterToken, AllyUserContract, ApiRequestContract, TwitterDriverConfig, - TwitterDriverContract, -} from '@ioc:Adonis/Addons/Ally' -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +} from '../types.js' +import { Oauth1Driver } from '../abstract_drivers/oauth1.js' /** * Twitter driver to login user via twitter */ -export class TwitterDriver - extends Oauth1Driver - implements TwitterDriverContract -{ +export class TwitterDriver extends Oauth1Driver { protected requestTokenUrl = 'https://api.twitter.com/oauth/request_token' protected authorizeUrl = 'https://api.twitter.com/oauth/authenticate' protected accessTokenUrl = 'https://api.twitter.com/oauth/access_token' @@ -61,7 +57,10 @@ export class TwitterDriver protected scopeParamName = '' protected scopesSeparator = ' ' - constructor(protected ctx: HttpContextContract, public config: TwitterDriverConfig) { + constructor( + protected ctx: HttpContext, + public config: TwitterDriverConfig + ) { super(ctx, config) /** @@ -115,7 +114,7 @@ export class TwitterDriver /** * Returns details for the authorized user */ - public async user(callback?: (request: ApiRequestContract) => void) { + async user(callback?: (request: ApiRequestContract) => void) { const token = await this.accessToken() const userInfo = await this.getUserInfo(token.token, token.secret, callback) @@ -129,7 +128,7 @@ export class TwitterDriver * Finds the user info from the "oauth_token" and "oauth_token_secret" * access from the access token. */ - public async userFromTokenAndSecret( + async userFromTokenAndSecret( token: string, secret: string, callback?: (request: ApiRequestContract) => void @@ -145,7 +144,7 @@ export class TwitterDriver /** * Find if the current error code is for access denied */ - public accessDenied(): boolean { + accessDenied(): boolean { return this.ctx.request.input('denied') } } diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..7564a97 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,22 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createError } from '@poppinss/utils' + +export const E_OAUTH_MISSING_CODE = createError<[string]>( + 'Cannot request access token. Redirect request is missing the "%s" param', + 'E_OAUTH_MISSING_CODE', + 500 +) + +export const E_OAUTH_STATE_MISMATCH = createError( + 'Unable to verify re-redirect state', + 'E_OAUTH_STATE_MISMATCH', + 400 +) diff --git a/src/redirect_request.ts b/src/redirect_request.ts new file mode 100644 index 0000000..0e07cfb --- /dev/null +++ b/src/redirect_request.ts @@ -0,0 +1,75 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { UrlBuilder } from '@poppinss/oauth-client' +import { LiteralStringUnion } from './types.js' + +/** + * Redirect request with first class support for defining scopes. + */ +export class RedirectRequest extends UrlBuilder { + #scopesTransformer: undefined | ((scopes: LiteralStringUnion[]) => string[]) + #scopeParamName: string + #scopeSeparator: string + + constructor(baseUrl: string, scopeParamName: string, scopeSeparator: string) { + super(baseUrl) + this.#scopeParamName = scopeParamName + this.#scopeSeparator = scopeSeparator + } + + /** + * Register a custom function to transform scopes. Exposed for drivers + * to implement. + */ + transformScopes(callback: (scopes: LiteralStringUnion[]) => string[]): this { + this.#scopesTransformer = callback + return this + } + + /** + * Define an array of scopes. + */ + scopes(scopes: LiteralStringUnion[]): this { + if (typeof this.#scopesTransformer === 'function') { + scopes = this.#scopesTransformer(scopes) + } + + this.param(this.#scopeParamName, scopes.join(this.#scopeSeparator)) + return this + } + + /** + * Merge to existing scopes + */ + mergeScopes(scopes: LiteralStringUnion[]): this { + if (typeof this.#scopesTransformer === 'function') { + scopes = this.#scopesTransformer(scopes) + } + + const existingScopes = this.getParams()[this.#scopeParamName] + const scopesString = scopes.join(this.#scopeSeparator) + + if (!existingScopes) { + this.param(this.#scopeParamName, scopesString) + return this + } + + this.param(this.#scopeParamName, `${existingScopes}${this.#scopeSeparator}${scopesString}`) + return this + } + + /** + * Clear existing scopes + */ + clearScopes(): this { + this.clearParam(this.#scopeParamName) + return this + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..79089a4 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,599 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { HttpContext } from '@adonisjs/core/http' +import type { ConfigProvider } from '@adonisjs/core/types' +import type { + Oauth2AccessToken, + Oauth1RequestToken, + Oauth1AccessToken, + Oauth1ClientConfig, + Oauth2ClientConfig, + ApiRequestContract, + RedirectRequestContract as ClientRequestContract, +} from '@poppinss/oauth-client/types' +import type { AllyManager } from './ally_manager.js' + +export type { Oauth2AccessToken } +export type { Oauth1AccessToken } +export type { Oauth1RequestToken } +export type { ApiRequestContract } +export type { Oauth2ClientConfig as Oauth2DriverConfig } +export type { Oauth1ClientConfig as Oauth1DriverConfig } + +/** + * Issue: https://github.com/Microsoft/TypeScript/issues/29729 + * Solution: https://github.com/sindresorhus/type-fest/blob/main/source/literal-union.d.ts + */ +export type LiteralStringUnion = LiteralType | (string & { _?: never }) + +/** + * Extension of oauth-client redirect request with support + * for defining scopes as first class citizen + */ +export interface RedirectRequestContract + extends ClientRequestContract { + /** + * Define a callback to transform scopes before they are defined + * as a param + */ + transformScopes(callback: (scopes: LiteralStringUnion[]) => string[]): this + + /** + * Define the scopes for authorization + */ + scopes(scopes: LiteralStringUnion[]): this + + /** + * Merge to existing pre-defined scopes + */ + mergeScopes(scopes: LiteralStringUnion[]): this + + /** + * Clear existing scopes + */ + clearScopes(): this +} + +/** + * The user fetched from the oauth provider. + */ +export interface AllyUserContract { + id: string + nickName: string + name: string + email: string | null + emailVerificationState: 'verified' | 'unverified' | 'unsupported' + avatarUrl: string | null + token: Token + original: any +} + +/** + * Every driver should implement this contract + */ +export interface AllyDriverContract< + Token extends Oauth2AccessToken | Oauth1AccessToken, + Scopes extends string, +> { + version: 'oauth1' | 'oauth2' + + /** + * Perform stateless authentication. Only applicable for Oauth2 clients + */ + stateless(): this + + /** + * Redirect user for authorization + */ + redirect(callback?: (request: RedirectRequestContract) => void): Promise + + /** + * Get redirect url. You must manage the state yourself when redirecting + * manually + */ + redirectUrl(callback?: (request: RedirectRequestContract) => void): Promise + + /** + * Find if the current request has authorization code or oauth token + */ + hasCode(): boolean + + /** + * Get the current request authorization code or oauth token. Returns + * null if there no code + */ + getCode(): string | null + + /** + * Find if the current error code is for access denied + */ + accessDenied(): boolean + + /** + * Find if there is a state mismatch + */ + stateMisMatch(): boolean + + /** + * Find if there is an error post redirect + */ + hasError(): boolean + + /** + * Get the post redirect error + */ + getError(): string | null + + /** + * Get access token + */ + accessToken(callback?: (request: ApiRequestContract) => void): Promise + + /** + * Returns details for the authorized user + */ + user(callback?: (request: ApiRequestContract) => void): Promise> + + /** + * Finds the user by access token. Applicable with "Oauth2" only + */ + userFromToken( + token: string, + callback?: (request: ApiRequestContract) => void + ): Promise> + + /** + * Finds the user by access token. Applicable with "Oauth1" only + */ + userFromTokenAndSecret( + token: string, + secret: string, + callback?: (request: ApiRequestContract) => void + ): Promise> +} + +/** + * The manager driver factory method is called by the AllyManager to create + * an instance of a driver during an HTTP request + */ +export type AllyManagerDriverFactory = (ctx: HttpContext) => AllyDriverContract + +/** + * ---------------------------------------- + * Discord driver + * ---------------------------------------- + */ + +/** + * Available discord scopes + * https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes + */ +export type DiscordScopes = + | 'activities.read' + | 'activities.write' + | 'applications.builds.read' + | 'applications.builds.upload' + | 'applications.commands' + | 'applications.commands.update' + | 'applications.entitlements' + | 'applications.store.update' + | 'bot' + | 'connections' + | 'email' + | 'gdm.join' + | 'guilds' + | 'guilds.join' + | 'identify' + | 'messages.read' + | 'relationships.read' + | 'rpc' + | 'rpc.activities.write' + | 'rpc.notifications.read' + | 'rpc.voice.read' + | 'rpc.voice.write' + | 'webhook.incoming' + +/** + * Shape of the Discord access token + */ +export type DiscordToken = { + token: string + type: string + scope: string + expiresIn: number + expiresAt: Exclude + refreshToken: string +} + +/** + * Extra options available for Discord + */ +export type DiscordDriverConfig = Oauth2ClientConfig & { + userInfoUrl?: string + scopes?: LiteralStringUnion[] + prompt?: 'consent' | 'none' + guildId?: `${bigint}` // a snowflake + disableGuildSelect?: boolean + permissions?: number +} + +/** + * ---------------------------------------- + * Github driver + * ---------------------------------------- + */ + +/** + * Available github scopes + * https://docs.github.com/en/free-pro-team@latest/developers/apps/scopes-for-oauth-apps#available-scopes + */ +export type GithubScopes = + | 'repo' + | 'repo:status' + | 'repo_deployment' + | 'public_repo' + | 'repo:invite' + | 'security_events' + | 'admin:repo_hook' + | 'write:repo_hook' + | 'read:repo_hook' + | 'admin:org' + | 'write:org' + | 'read:org' + | 'admin:public_key' + | 'write:public_key' + | 'read:public_key' + | 'admin:org_hook' + | 'gist' + | 'notifications' + | 'user' + | 'read:user' + | 'user:email' + | 'user:follow' + | 'delete_repo' + | 'write:discussion' + | 'read:discussion' + | 'write:packages' + | 'read:packages' + | 'delete:packages' + | 'admin:gpg_key' + | 'write:gpg_key' + | 'read:gpg_key' + | 'workflow' + +/** + * Shape of the Github access token + */ +export type GithubToken = { + token: string + type: string + scope: string +} + +/** + * Extra options available for Github + */ +export type GithubDriverConfig = Oauth2ClientConfig & { + login?: string + scopes?: LiteralStringUnion[] + allowSignup?: boolean + userInfoUrl?: string + userEmailUrl?: string +} + +/** + * ---------------------------------------- + * Twitter driver + * ---------------------------------------- + */ + +/** + * Shape of the twitter token + */ +export type TwitterToken = { + token: string + secret: string + userId: string + screenName: string +} + +/** + * Extra options available for twitter + */ +export type TwitterDriverConfig = Oauth1ClientConfig & { + userInfoUrl?: string +} + +/** + * ---------------------------------------- + * Google driver + * ---------------------------------------- + */ + +/** + * Most popular google scopes. You can find rest of them on the following link + * https://developers.google.com/identity/protocols/oauth2/scopes + */ +export type GoogleScopes = + | 'userinfo.email' + | 'userinfo.profile' + | 'openid' + | 'contacts' + | 'contacts.other.readonly' + | 'contacts.readonly' + | 'directory.readonly' + | 'user.addresses.read' + | 'user.birthday.read' + | 'user.emails.read' + | 'user.gender.read' + | 'user.organization.read' + | 'user.phonenumbers.read' + | 'analytics' + | 'analytics.readonly' + | 'documents' + | 'documents.readonly' + | 'forms' + | 'forms.currentonly' + | 'groups' + | 'spreadsheets' + | 'calendar' + | 'calendar.events' + | 'calendar.events.readonly' + | 'calendar.readonly' + | 'calendar.settings.readonly' + | 'drive' + | 'drive.appdata' + | 'drive.file' + | 'drive.metadata' + | 'drive.metadata.readonly' + | 'drive.photos.readonly' + | 'drive.readonly' + | 'drive.scripts' + +/** + * Shape of the Google access token + */ +export type GoogleToken = Oauth2AccessToken & { + /** + * @deprecated + * Use `idToken` instead + */ + id_token: string + idToken: string + grantedScopes: string[] +} + +/** + * Config accepted by the google driver. Most of the options can be + * overwritten at runtime + * https://developers.google.com/identity/protocols/oauth2/openid-connect#re-consent + */ +export type GoogleDriverConfig = Oauth2ClientConfig & { + userInfoUrl?: string + + /** + * Can be configured at runtime + */ + scopes?: LiteralStringUnion[] + prompt?: 'none' | 'consent' | 'select_account' + accessType?: 'online' | 'offline' + hostedDomain?: string + display?: 'page' | 'popup' | 'touch' | 'wrap' +} + +/** + * ---------------------------------------- + * LinkedIn driver + * ---------------------------------------- + */ + +/** + * A list of LinkedIn scopes. You can find the scopes available + * to your app from the LinkedIn dasbhoard. + * https://www.linkedin.com/developers/apps//auth + */ +export type LinkedInScopes = + | 'r_emailaddress' + | 'r_liteprofile' + | 'w_member_social' + | 'r_fullprofile' + | 'r_basicprofile_app' + | 'r_primarycontact' + | 'rw_organization_admin' + +/** + * Shape of the Linked access token + */ +export type LinkedInToken = { + token: string + type: string + expiresIn: number + expiresAt: Exclude +} + +/** + * Config accepted by the linkedin driver. Most of the options can be + * overwritten at runtime + * https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext&tabs=HTTPS#step-2-request-an-authorization-code + */ +export type LinkedInDriverConfig = Oauth2ClientConfig & { + userInfoUrl?: string + userEmailUrl?: string + + /** + * Can be configured at runtime + */ + scopes?: LiteralStringUnion[] +} + +/** + * ---------------------------------------- + * Facebook driver + * ---------------------------------------- + */ + +/** + * Most popular facebook scopes. You can find rest of them on the following link + * https://developers.facebook.com/docs/permissions/reference/ + */ +export type FacebookScopes = + | 'ads_management' + | 'ads_read' + | 'attribution_read' + | 'business_management' + | 'catalog_management' + | 'email' + | 'groups_access_member_info' + | 'leads_retrieval' + | 'pages_events' + | 'pages_manage_ads' + | 'pages_manage_cta' + | 'pages_manage_instant_articles' + | 'pages_manage_engagement' + | 'pages_manage_metadata' + | 'pages_manage_posts' + | 'pages_messaging' + | 'pages_read_engagement' + | 'pages_read_user_content' + | 'pages_show_list' + | 'pages_user_gender' + | 'pages_user_locale' + | 'pages_user_timezone' + | 'public_profile' + | 'publish_to_groups' + | 'publish_video' + | 'read_insights' + | 'user_age_range' + | 'user_birthday' + | 'user_friends' + | 'user_gender' + | 'user_hometown' + | 'user_likes' + | 'user_link' + | 'user_location' + | 'user_photos' + | 'user_posts' + | 'user_videos' + +/** + * Most used user profile fields. + * For more visit https://developers.facebook.com/docs/graph-api/reference/user + */ +export type FacebookProfileFields = + | 'id' + | 'first_name' + | 'last_name' + | 'middle_name' + | 'name' + | 'name_format' + | 'picture' + | 'short_name' + | 'verified' + | 'birthday' + | 'email' + | 'gender' + | 'link' + +/** + * Shape of the Facebook access token + */ +export type FacebookToken = { + token: string + type: string + expiresIn: number + expiresAt: Exclude +} + +/** + * Config accepted by the facebook driver. Most of the options can be + * overwritten at runtime + * https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow + */ +export type FacebookDriverConfig = Oauth2ClientConfig & { + userInfoUrl?: string + + /** + * Can be configured at runtime + */ + scopes?: LiteralStringUnion[] + userFields?: LiteralStringUnion[] + display?: string + authType?: string +} + +/** + * ---------------------------------------- + * Spotify driver + * ---------------------------------------- + */ + +/** + * Available spotify scopes + * https://developer.spotify.com/documentation/general/guides/scopes/ + */ +export type SpotifyScopes = + | 'ugc-image-upload' + | 'user-read-recently-played' + | 'user-top-read' + | 'user-read-playback-position' + | 'user-read-playback-state' + | 'user-modify-playback-state' + | 'user-read-currently-playing' + | 'app-remote-control' + | 'streaming' + | 'playlist-modify-public' + | 'playlist-modify-private' + | 'playlist-read-private' + | 'playlist-read-collaborative' + | 'user-follow-modify' + | 'user-follow-read' + | 'user-library-modify' + | 'user-library-read' + | 'user-read-email' + | 'user-read-private' + +/** + * Shape of the Spotify access token + */ +export type SpotifyToken = { + token: string + type: string + refreshToken: string + expiresIn: number + expiresAt: Exclude +} + +/** + * Extra options available for Spotify + */ +export type SpotifyDriverConfig = Oauth2ClientConfig & { + scopes?: LiteralStringUnion[] + showDialog?: boolean +} +/** + * END OF DRIVERS + */ + +/** + * Social providers are inferred inside the user application + * from the config file + */ +export interface SocialProviders {} +export type InferSocialProviders< + T extends ConfigProvider>, +> = Awaited> + +/** + * Ally service is shared with the HTTP context + */ +export interface AllyService + extends AllyManager< + SocialProviders extends Record ? SocialProviders : never + > {} diff --git a/standalone.ts b/standalone.ts deleted file mode 100644 index 34976e7..0000000 --- a/standalone.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/** - * Exports required to create a custom driver - */ -export { HttpClient as ApiRequest } from '@poppinss/oauth-client' -export { OauthException } from './src/Exceptions' -export { Oauth2Driver } from './src/AbstractDrivers/Oauth2' -export { RedirectRequest } from './src/RedirectRequest' diff --git a/stubs/config/ally.stub b/stubs/config/ally.stub new file mode 100644 index 0000000..d5ebfdd --- /dev/null +++ b/stubs/config/ally.stub @@ -0,0 +1,21 @@ +{{{ + exports({ to: app.configPath('ally.ts') }) +}}} +import env from '#start/env' +import { defineConfig, services } from '@adonisjs/ally' + +const allyConfig = defineConfig({ +{{#each providers as provider}} + {{provider.provider}}: services.{{provider.provider}}({ + clientId: env.get('{{provider.envPrefix}}_CLIENT_ID'), + clientSecret: env.get('{{provider.envPrefix}}_CLIENT_SECRET'), + callbackUrl: '', + }), +{{/each}} +}) + +export default allyConfig + +declare module '@adonisjs/ally/types' { + interface SocialProviders extends InferSocialProviders {} +} diff --git a/stubs/main.ts b/stubs/main.ts new file mode 100644 index 0000000..2fc8529 --- /dev/null +++ b/stubs/main.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { getDirname } from '@poppinss/utils' + +export const stubsRoot = getDirname(import.meta.url) diff --git a/templates/config/ally.txt b/templates/config/ally.txt deleted file mode 100644 index fbad703..0000000 --- a/templates/config/ally.txt +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Config source: https://git.io/JOdi5 - * - * Feel free to let us know via PR, if you find something broken in this config - * file. - */ - -import Env from '@ioc:Adonis/Core/Env' -import { AllyConfig } from '@ioc:Adonis/Addons/Ally' - -/* -|-------------------------------------------------------------------------- -| Ally Config -|-------------------------------------------------------------------------- -| -| The `AllyConfig` relies on the `SocialProviders` interface which is -| defined inside `contracts/ally.ts` file. -| -*/ -const allyConfig: AllyConfig = { -{{#providers.github}} -{{> github_provider}} -{{/providers.github}} -{{#providers.google}} -{{> google_provider}} -{{/providers.google}} -{{#providers.twitter}} -{{> twitter_provider}} -{{/providers.twitter}} -{{#providers.discord}} -{{> discord_provider}} -{{/providers.discord}} -{{#providers.linkedin}} -{{> linkedin_provider}} -{{/providers.linkedin}} -{{#providers.facebook}} -{{> facebook_provider}} -{{/providers.facebook}} -{{#providers.spotify}} -{{> spotify_provider}} -{{/providers.spotify}} -} - -export default allyConfig diff --git a/templates/config/partials/discord.txt b/templates/config/partials/discord.txt deleted file mode 100644 index d88dcdb..0000000 --- a/templates/config/partials/discord.txt +++ /dev/null @@ -1,11 +0,0 @@ - /* - |-------------------------------------------------------------------------- - | Discord driver - |-------------------------------------------------------------------------- - */ - discord: { - driver: 'discord', - clientId: Env.get('{{ envVars.discord.clientId }}'), - clientSecret: Env.get('{{ envVars.discord.clientSecret }}'), - callbackUrl: 'http://localhost:3333/discord/callback', - }, diff --git a/templates/config/partials/facebook.txt b/templates/config/partials/facebook.txt deleted file mode 100644 index 34981ee..0000000 --- a/templates/config/partials/facebook.txt +++ /dev/null @@ -1,11 +0,0 @@ - /* - |-------------------------------------------------------------------------- - | Facebook driver - |-------------------------------------------------------------------------- - */ - facebook: { - driver: 'facebook', - clientId: Env.get('{{ envVars.facebook.clientId }}'), - clientSecret: Env.get('{{ envVars.facebook.clientSecret }}'), - callbackUrl: 'http://localhost:3333/facebook/callback', - }, diff --git a/templates/config/partials/github.txt b/templates/config/partials/github.txt deleted file mode 100644 index e4f2ed4..0000000 --- a/templates/config/partials/github.txt +++ /dev/null @@ -1,11 +0,0 @@ - /* - |-------------------------------------------------------------------------- - | Github driver - |-------------------------------------------------------------------------- - */ - github: { - driver: 'github', - clientId: Env.get('{{ envVars.github.clientId }}'), - clientSecret: Env.get('{{ envVars.github.clientSecret }}'), - callbackUrl: 'http://localhost:3333/github/callback', - }, diff --git a/templates/config/partials/google.txt b/templates/config/partials/google.txt deleted file mode 100644 index a2e6630..0000000 --- a/templates/config/partials/google.txt +++ /dev/null @@ -1,11 +0,0 @@ - /* - |-------------------------------------------------------------------------- - | Google driver - |-------------------------------------------------------------------------- - */ - google: { - driver: 'google', - clientId: Env.get('{{ envVars.google.clientId }}'), - clientSecret: Env.get('{{ envVars.google.clientSecret }}'), - callbackUrl: 'http://localhost:3333/google/callback', - }, diff --git a/templates/config/partials/linkedin.txt b/templates/config/partials/linkedin.txt deleted file mode 100644 index 67aabc9..0000000 --- a/templates/config/partials/linkedin.txt +++ /dev/null @@ -1,11 +0,0 @@ - /* - |-------------------------------------------------------------------------- - | LinkedIn driver - |-------------------------------------------------------------------------- - */ - linkedin: { - driver: 'linkedin', - clientId: Env.get('{{ envVars.linkedin.clientId }}'), - clientSecret: Env.get('{{ envVars.linkedin.clientSecret }}'), - callbackUrl: 'http://localhost:3333/linkedin/callback', - }, diff --git a/templates/config/partials/spotify.txt b/templates/config/partials/spotify.txt deleted file mode 100644 index 08758f0..0000000 --- a/templates/config/partials/spotify.txt +++ /dev/null @@ -1,12 +0,0 @@ - /* - |-------------------------------------------------------------------------- - | Spotify driver - |-------------------------------------------------------------------------- - */ - spotify: { - driver: 'spotify', - clientId: Env.get('{{ envVars.spotify.clientId }}'), - clientSecret: Env.get('{{ envVars.spotify.clientSecret }}'), - callbackUrl: 'http://localhost:3333/spotify/callback', - }, - diff --git a/templates/config/partials/twitter.txt b/templates/config/partials/twitter.txt deleted file mode 100644 index bb849e6..0000000 --- a/templates/config/partials/twitter.txt +++ /dev/null @@ -1,11 +0,0 @@ - /* - |-------------------------------------------------------------------------- - | Twitter driver - |-------------------------------------------------------------------------- - */ - twitter: { - driver: 'twitter', - clientId: Env.get('{{ envVars.twitter.clientId }}'), - clientSecret: Env.get('{{ envVars.twitter.clientSecret }}'), - callbackUrl: 'http://localhost:3333/twitter/callback', - }, diff --git a/templates/contracts/ally.txt b/templates/contracts/ally.txt deleted file mode 100644 index b54a3c3..0000000 --- a/templates/contracts/ally.txt +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Contract source: https://git.io/JOdiQ - * - * Feel free to let us know via PR, if you find something broken in this contract - * file. - */ - -declare module '@ioc:Adonis/Addons/Ally' { - interface SocialProviders { - {{#providers.github}} - github: { - config: GithubDriverConfig - implementation: GithubDriverContract - } - {{/providers.github}} - {{#providers.google}} - google: { - config: GoogleDriverConfig - implementation: GoogleDriverContract - } - {{/providers.google}} - {{#providers.twitter}} - twitter: { - config: TwitterDriverConfig - implementation: TwitterDriverContract - } - {{/providers.twitter}} - {{#providers.discord}} - discord: { - config: DiscordDriverConfig - implementation: DiscordDriverContract - } - {{/providers.discord}} - {{#providers.linkedin}} - linkedin: { - config: LinkedInDriverConfig - implementation: LinkedInDriverContract - } - {{/providers.linkedin}} - {{#providers.facebook}} - facebook: { - config: FacebookDriverConfig - implementation: FacebookDriverContract - } - {{/providers.facebook}} - {{#providers.spotify}} - spotify: { - config: SpotifyDriverConfig - implementation: SpotifyDriverContract - } - {{/providers.spotify}} - } -} diff --git a/test-helpers/contracts.ts b/test-helpers/contracts.ts deleted file mode 100644 index 2cd1490..0000000 --- a/test-helpers/contracts.ts +++ /dev/null @@ -1,32 +0,0 @@ -declare module '@ioc:Adonis/Addons/Ally' { - interface SocialProviders { - discord: { - config: DiscordDriverConfig - implementation: DiscordDriverContract - } - github: { - config: GithubDriverConfig - implementation: GithubDriverContract - } - twitter: { - config: TwitterDriverConfig - implementation: TwitterDriverContract - } - linkedin: { - config: LinkedInDriverConfig - implementation: LinkedInDriverContract - } - google: { - config: GoogleDriverConfig - implementation: GoogleDriverContract - } - facebook: { - config: FacebookDriverConfig - implementation: FacebookDriverContract - } - spotify: { - config: SpotifyDriverConfig - implementation: SpotifyDriverContract - } - } -} diff --git a/test-helpers/index.ts b/test-helpers/index.ts deleted file mode 100644 index aef750e..0000000 --- a/test-helpers/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/core/build/standalone' - -export const fs = new Filesystem(join(__dirname, '__app')) - -export async function setup(setupProviders?: boolean) { - const application = new Application(fs.basePath, 'web', { - providers: ['@adonisjs/core', '../../providers/AllyProvider'], - }) - - await fs.add( - 'config/app.ts', - ` - export const profiler = { enabled: true } - export const appKey = 'averylongrandomsecretkey' - export const http = { - trustProxy: () => {}, - cookie: {} - } - ` - ) - - await application.setup() - - if (setupProviders) { - await application.registerProviders() - await application.bootProviders() - } - - return application -} diff --git a/test/ally-manager.spec.ts b/test/ally-manager.spec.ts deleted file mode 100644 index 8df7021..0000000 --- a/test/ally-manager.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * @adonisjs/ally - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { Ally } from '../src/Ally' -import { AllyManager } from '../src/AllyManager' -import { GithubDriver } from '../src/Drivers/Github' - -import { setup, fs } from '../test-helpers' - -test.group('AllyManager', (group) => { - group.teardown(async () => { - await fs.cleanup() - }) - - test('make instance of a mapping', async ({ assert }) => { - const app = await setup(true) - const manager = new AllyManager(app, { - github: { - driver: 'github', - }, - }) - - const HttpContext = app.container.resolveBinding('Adonis/Core/HttpContext') - assert.instanceOf(manager.makeMapping(HttpContext.create('/', {}), 'github'), GithubDriver) - }) - - test('register provider as singleton', async ({ assert }) => { - const app = await setup(true) - assert.strictEqual( - app.container.resolveBinding('Adonis/Addons/Ally'), - app.container.resolveBinding('Adonis/Addons/Ally') - ) - }) - - test('add ally getter to http context', async ({ assert }) => { - const app = await setup(true) - const HttpContext = app.container.resolveBinding('Adonis/Core/HttpContext') - - assert.instanceOf(HttpContext.create('/', {}).ally, Ally) - }) - - test('extend ally manager to add custom drivers', async ({ assert }) => { - const app = await setup(true) - class MyCustomDriver {} - - const manager = new AllyManager(app, { - foo: { - driver: 'foo', - }, - }) - manager.extend('foo', () => { - return new MyCustomDriver() as any - }) - - const HttpContext = app.container.resolveBinding('Adonis/Core/HttpContext') - assert.instanceOf( - manager.makeMapping(HttpContext.create('/', {}), 'foo' as any), - MyCustomDriver - ) - }) -}) diff --git a/tests/ally_manager.spec.ts b/tests/ally_manager.spec.ts new file mode 100644 index 0000000..2e95920 --- /dev/null +++ b/tests/ally_manager.spec.ts @@ -0,0 +1,47 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { HttpContextFactory } from '@adonisjs/core/factories/http' + +import { AllyManager } from '../src/ally_manager.js' +import { GithubDriver } from '../src/drivers/github.js' + +test.group('Ally manager', () => { + test('create an instance of a driver', ({ assert, expectTypeOf }) => { + const ctx = new HttpContextFactory().create() + + const ally = new AllyManager( + { + github: ($ctx) => { + return new GithubDriver($ctx, { + clientId: '', + clientSecret: '', + callbackUrl: '', + }) + }, + }, + ctx + ) + + assert.instanceOf(ally.use('github'), GithubDriver) + assert.strictEqual(ally.use('github'), ally.use('github')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['github']>() + expectTypeOf(ally.use('github')).toMatchTypeOf() + }) + + test('throw error when making an unknown driver', () => { + const ctx = new HttpContextFactory().create() + + const ally = new AllyManager({}, ctx) + ;(ally.use as any)('github') + }).throws( + 'Unknown ally provider "github". Make sure it is registered inside the config/ally.ts file' + ) +}) diff --git a/tests/ally_provider.spec.ts b/tests/ally_provider.spec.ts new file mode 100644 index 0000000..d61249b --- /dev/null +++ b/tests/ally_provider.spec.ts @@ -0,0 +1,52 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { IgnitorFactory } from '@adonisjs/core/factories' +import { HttpContextFactory } from '@adonisjs/core/factories/http' + +import { AllyManager } from '../src/ally_manager.js' +import { defineConfig, services } from '../src/define_config.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Ally provider', () => { + test('define HttpContext.ally property', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: [() => import('../providers/ally_provider.js')], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + ally: defineConfig({ + github: services.github({ + clientId: '', + clientSecret: '', + callbackUrl: '', + }), + }), + }, + }) + .create(BASE_URL) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + const ctx = new HttpContextFactory().create() + assert.instanceOf(ctx.ally, AllyManager) + + // Should be singleton + assert.strictEqual(ctx.ally, ctx.ally) + }) +}) diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts new file mode 100644 index 0000000..c8d0d29 --- /dev/null +++ b/tests/configure.spec.ts @@ -0,0 +1,229 @@ +/* + * @adonisjs/static + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { fileURLToPath } from 'node:url' +import { IgnitorFactory } from '@adonisjs/core/factories' +import Configure from '@adonisjs/core/commands/configure' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Configure', (group) => { + group.each.setup(({ context }) => { + context.fs.baseUrl = BASE_URL + context.fs.basePath = fileURLToPath(BASE_URL) + }) + + group.each.disableTimeout() + + test('do not prompt when provider is provided as a CLI flag', async ({ fs, assert }) => { + const ignitor = new IgnitorFactory() + .withCoreProviders() + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + + return import(filePath) + }, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + await fs.create('.env', '') + await fs.createJson('tsconfig.json', {}) + await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await app.container.make('ace') + const command = await ace.create(Configure, ['../../index.js', '--providers=facebook']) + await command.exec() + + await assert.fileExists('config/ally.ts') + await assert.fileExists('adonisrc.ts') + await assert.fileContains('adonisrc.ts', '@adonisjs/ally/ally_provider') + await assert.fileContains('config/ally.ts', 'defineConfig') + await assert.fileContains('config/ally.ts', `declare module '@adonisjs/ally/types' {`) + await assert.fileContains( + 'config/ally.ts', + `facebook: services.facebook({ + clientId: env.get('FACEBOOK_CLIENT_ID'), + clientSecret: env.get('FACEBOOK_CLIENT_SECRET'), + callbackUrl: '', + }),` + ) + await assert.fileContains('.env', 'FACEBOOK_CLIENT_ID') + await assert.fileContains('.env', 'FACEBOOK_CLIENT_SECRET') + + await assert.fileContains('start/env.ts', 'FACEBOOK_CLIENT_ID: Env.schema.string()') + await assert.fileContains('start/env.ts', 'FACEBOOK_CLIENT_SECRET: Env.schema.string()') + }) + + test('define multiple providers via the CLI flag', async ({ fs, assert }) => { + const ignitor = new IgnitorFactory() + .withCoreProviders() + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + + return import(filePath) + }, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + await fs.create('.env', '') + await fs.createJson('tsconfig.json', {}) + await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await app.container.make('ace') + const command = await ace.create(Configure, [ + '../../index.js', + '--providers=github', + '--providers=linkedin', + ]) + await command.exec() + + await assert.fileExists('config/ally.ts') + await assert.fileExists('adonisrc.ts') + await assert.fileContains('adonisrc.ts', '@adonisjs/ally/ally_provider') + await assert.fileContains('config/ally.ts', 'defineConfig') + await assert.fileContains('config/ally.ts', `declare module '@adonisjs/ally/types' {`) + await assert.fileContains( + 'config/ally.ts', + `github: services.github({ + clientId: env.get('GITHUB_CLIENT_ID'), + clientSecret: env.get('GITHUB_CLIENT_SECRET'), + callbackUrl: '', + }),` + ) + await assert.fileContains( + 'config/ally.ts', + `linkedin: services.linkedin({ + clientId: env.get('LINKEDIN_CLIENT_ID'), + clientSecret: env.get('LINKEDIN_CLIENT_SECRET'), + callbackUrl: '', + }),` + ) + await assert.fileContains('.env', 'GITHUB_CLIENT_ID') + await assert.fileContains('.env', 'GITHUB_CLIENT_SECRET') + await assert.fileContains('.env', 'LINKEDIN_CLIENT_ID') + await assert.fileContains('.env', 'LINKEDIN_CLIENT_SECRET') + + await assert.fileContains('start/env.ts', 'GITHUB_CLIENT_ID: Env.schema.string()') + await assert.fileContains('start/env.ts', 'GITHUB_CLIENT_SECRET: Env.schema.string()') + await assert.fileContains('start/env.ts', 'LINKEDIN_CLIENT_ID: Env.schema.string()') + await assert.fileContains('start/env.ts', 'LINKEDIN_CLIENT_SECRET: Env.schema.string()') + }) + + test('report error when CLI provider is invalid', async ({ fs, assert }) => { + const ignitor = new IgnitorFactory() + .withCoreProviders() + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + + return import(filePath) + }, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + await fs.create('.env', '') + await fs.createJson('tsconfig.json', {}) + await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await app.container.make('ace') + const command = await ace.create(Configure, ['../../index.js', '--providers=foo']) + await command.exec() + + command.assertFailed() + + await assert.fileNotExists('config/ally.ts') + await assert.fileEquals('.env', '') + await assert.fileEquals('start/env.ts', `export default Env.create(new URL('./'), {})`) + }) + + test('prompt when provider is pre-defined', async ({ fs, assert }) => { + const ignitor = new IgnitorFactory() + .withCoreProviders() + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + + return import(filePath) + }, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + await fs.create('.env', '') + await fs.createJson('tsconfig.json', {}) + await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await app.container.make('ace') + ace.prompt.trap('Select the social auth providers you plan to use').chooseOptions([2, 4]) + + const command = await ace.create(Configure, ['../../index.js']) + await command.exec() + + await assert.fileExists('config/ally.ts') + await assert.fileExists('adonisrc.ts') + await assert.fileContains('adonisrc.ts', '@adonisjs/ally/ally_provider') + await assert.fileContains('config/ally.ts', 'defineConfig') + await assert.fileContains('config/ally.ts', `declare module '@adonisjs/ally/types' {`) + await assert.fileContains( + 'config/ally.ts', + `github: services.github({ + clientId: env.get('GITHUB_CLIENT_ID'), + clientSecret: env.get('GITHUB_CLIENT_SECRET'), + callbackUrl: '', + }),` + ) + await assert.fileContains( + 'config/ally.ts', + `linkedin: services.linkedin({ + clientId: env.get('LINKEDIN_CLIENT_ID'), + clientSecret: env.get('LINKEDIN_CLIENT_SECRET'), + callbackUrl: '', + }),` + ) + await assert.fileContains('.env', 'GITHUB_CLIENT_ID') + await assert.fileContains('.env', 'GITHUB_CLIENT_SECRET') + await assert.fileContains('.env', 'LINKEDIN_CLIENT_ID') + await assert.fileContains('.env', 'LINKEDIN_CLIENT_SECRET') + + await assert.fileContains('start/env.ts', 'GITHUB_CLIENT_ID: Env.schema.string()') + await assert.fileContains('start/env.ts', 'GITHUB_CLIENT_SECRET: Env.schema.string()') + await assert.fileContains('start/env.ts', 'LINKEDIN_CLIENT_ID: Env.schema.string()') + await assert.fileContains('start/env.ts', 'LINKEDIN_CLIENT_SECRET: Env.schema.string()') + }) +}) diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts new file mode 100644 index 0000000..3862c12 --- /dev/null +++ b/tests/define_config.spec.ts @@ -0,0 +1,181 @@ +/* + * @adonisjs/ally + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AppFactory } from '@adonisjs/core/factories/app' +import { ApplicationService } from '@adonisjs/core/types' +import { HttpContextFactory } from '@adonisjs/core/factories/http' + +import { AllyManager } from '../src/ally_manager.js' +import { GoogleDriver } from '../src/drivers/google.js' +import { GithubDriver } from '../src/drivers/github.js' +import { services, defineConfig } from '../src/define_config.js' +import { DiscordDriver } from '../src/drivers/discord.js' +import { FacebookDriver } from '../src/drivers/facebook.js' +import { LinkedInDriver } from '../src/drivers/linked_in.js' +import { SpotifyDriver } from '../src/drivers/spotify.js' +import { TwitterDriver } from '../src/drivers/twitter.js' + +const BASE_URL = new URL('./', import.meta.url) +const app = new AppFactory().create(BASE_URL, () => {}) as ApplicationService + +test.group('Define config', () => { + test('transform user defined config', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + github: services.github({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('github'), GithubDriver) + assert.strictEqual(ally.use('github'), ally.use('github')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['github']>() + expectTypeOf(ally.use('github')).toMatchTypeOf() + }) +}) + +test.group('Config services', () => { + test('configure github driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + github: services.github({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('github'), GithubDriver) + assert.strictEqual(ally.use('github'), ally.use('github')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['github']>() + expectTypeOf(ally.use('github')).toMatchTypeOf() + }) + + test('configure google driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + google: services.google({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('google'), GoogleDriver) + assert.strictEqual(ally.use('google'), ally.use('google')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['google']>() + expectTypeOf(ally.use('google')).toMatchTypeOf() + }) + + test('configure discord driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + discord: services.discord({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('discord'), DiscordDriver) + assert.strictEqual(ally.use('discord'), ally.use('discord')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['discord']>() + expectTypeOf(ally.use('discord')).toMatchTypeOf() + }) + + test('configure facebook driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + facebook: services.facebook({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('facebook'), FacebookDriver) + assert.strictEqual(ally.use('facebook'), ally.use('facebook')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['facebook']>() + expectTypeOf(ally.use('facebook')).toMatchTypeOf() + }) + + test('configure linkedin driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + linkedin: services.linkedin({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('linkedin'), LinkedInDriver) + assert.strictEqual(ally.use('linkedin'), ally.use('linkedin')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['linkedin']>() + expectTypeOf(ally.use('linkedin')).toMatchTypeOf() + }) + + test('configure spotify driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + spotify: services.spotify({ + clientId: '', + clientSecret: '', + callbackUrl: '', + scopes: ['admin:org'], + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('spotify'), SpotifyDriver) + assert.strictEqual(ally.use('spotify'), ally.use('spotify')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['spotify']>() + expectTypeOf(ally.use('spotify')).toMatchTypeOf() + }) + + test('configure twitter driver', async ({ assert, expectTypeOf }) => { + const managerConfig = await defineConfig({ + twitter: services.twitter({ + clientId: '', + clientSecret: '', + callbackUrl: '', + }), + }).resolver(app) + + const ctx = new HttpContextFactory().create() + const ally = new AllyManager(managerConfig, ctx) + + assert.instanceOf(ally.use('twitter'), TwitterDriver) + assert.strictEqual(ally.use('twitter'), ally.use('twitter')) + expectTypeOf(ally.use).parameters.toEqualTypeOf<['twitter']>() + expectTypeOf(ally.use('twitter')).toMatchTypeOf() + }) +}) diff --git a/tests/redirect_request.spec.ts b/tests/redirect_request.spec.ts new file mode 100644 index 0000000..6cd494c --- /dev/null +++ b/tests/redirect_request.spec.ts @@ -0,0 +1,55 @@ +/* + * @adonisjs/ally + * + * (c) Ally + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { RedirectRequest } from '../src/redirect_request.js' + +test.group('Redirect request', () => { + test('define scopes param', ({ assert }) => { + const redirect = new RedirectRequest('http://foo.com', 'scopes', ',') + redirect.scopes(['username', 'email']) + + assert.deepEqual(redirect.getParams(), { scopes: ['username', 'email'].join(',') }) + }) + + test('merge to existing scopes', ({ assert }) => { + const redirect = new RedirectRequest('http://foo.com', 'scopes', ',') + redirect.scopes(['username', 'email']) + redirect.mergeScopes(['avatar_url']) + + assert.deepEqual(redirect.getParams(), { + scopes: ['username', 'email', 'avatar_url'].join(','), + }) + }) + + test('clear existing scopes', ({ assert }) => { + const redirect = new RedirectRequest('http://foo.com', 'scopes', ',') + redirect.scopes(['username', 'email']) + redirect.clearScopes() + redirect.mergeScopes(['avatar_url']) + + assert.deepEqual(redirect.getParams(), { + scopes: ['avatar_url'].join(','), + }) + }) + + test('use scopes transformer', ({ assert }) => { + const redirect = new RedirectRequest('http://foo.com', 'scopes', ',') + redirect.transformScopes((scopes) => { + return scopes.map((scope) => `foo.com/${scope}`) + }) + + redirect.scopes(['username', 'email']) + redirect.mergeScopes(['avatar_url']) + + assert.deepEqual(redirect.getParams(), { + scopes: ['foo.com/username', 'foo.com/email', 'foo.com/avatar_url'].join(','), + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 3c150aa..ad0cc44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,7 @@ { - "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", + "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { - "types": [ - "@types/node", - "@adonisjs/core" - ], - "skipLibCheck": true + "rootDir": "./", + "outDir": "./build" } }