Compare commits

..

59 Commits

Author SHA1 Message Date
Lakhan Samani
3d75a4a281 Fix bug getting github login meta data 2022-01-10 10:45:20 +05:30
Lakhan Samani
2d819f5d3c Update app 2022-01-08 23:01:52 +05:30
Lakhan Samani
3221740198 Fix issue with reset password 2022-01-08 23:01:06 +05:30
Lakhan Samani
1e759c64ed Fix email template 2022-01-08 18:44:19 +05:30
Lakhan Samani
b3c7f783ed Update tests 2022-01-08 18:16:26 +05:30
Lakhan Samani
37fe5071c5 Use gomail library for sending emails
3b881776b5
2022-01-08 14:08:42 +05:30
Lakhan Samani
2137d8ef5d Merge pull request #96 from authorizerdev/fix/smtp-send-mail
fix: send mail from param
2022-01-07 21:40:52 +05:30
Lakhan Samani
8178fa6b62 fix: send mail from param 2022-01-07 21:40:28 +05:30
Lakhan Samani
303a3cbbbe Merge pull request #95 from authorizerdev/smtp-improvements
Improve smtp variable names
2022-01-07 19:32:20 +05:30
Lakhan Samani
9173b340c8 Improve smtp variable names
_Rename:_

- `SENDER_EMAIL` -> `SMTP_USERNAME`
- `SENDER_PASSWORD` -> `SMTP_PASSWORD`
- `SENDER_EMAIL` -> Used as `From` in email

Resolves #94
2022-01-07 19:29:22 +05:30
Lakhan Samani
df192bed4d feat: allow disabling login page
Resolves #89
2021-12-29 05:41:39 +05:30
Lakhan Samani
8d2371c14e fix: spacing 2021-12-29 05:21:24 +05:30
Lakhan Samani
5ed669e0da Merge pull request #88 from authorizerdev/fix/app-code-spliting
Fix/app code spliting
2021-12-29 05:19:20 +05:30
Lakhan Samani
43dce69cce fix: update build script 2021-12-29 05:10:21 +05:30
Lakhan Samani
4c53eb97d2 fix: enable code spliting for app 2021-12-29 04:16:31 +05:30
Lakhan Samani
b4b8593879 fix: default role values 2021-12-24 18:42:32 +05:30
Lakhan Samani
46a91fde20 fix: version var in make file 2021-12-24 17:47:35 +05:30
Lakhan Samani
8f826e6c2f Update CONTRIBUTING.md 2021-12-24 10:55:07 +05:30
Lakhan Samani
ebfea707c5 Update README.md 2021-12-24 10:06:39 +05:30
Lakhan Samani
8fc0175166 Update CONTRIBUTING.md 2021-12-24 10:05:05 +05:30
Lakhan Samani
dc43f56db1 fix: update authorizer-react version 2021-12-24 09:55:24 +05:30
Lakhan Samani
e5761f1e42 Merge pull request #87 from authorizerdev/feat/use-opend-id-standard-claims
feat/use opend id standard claims
2021-12-24 08:49:01 +05:30
Lakhan Samani
8dd8252a46 fix: move test to __test__ folder 2021-12-24 07:40:04 +05:30
Lakhan Samani
7ee4715af2 fix: rename magic_link_login enum 2021-12-24 07:20:22 +05:30
Lakhan Samani
1b3f931074 fix: rename getresuser util 2021-12-24 06:35:02 +05:30
Lakhan Samani
30cde3e521 feat: add tests for all resolvers 2021-12-24 06:27:39 +05:30
Lakhan Samani
6e9370458b fix: create common resolver test suite 2021-12-23 14:17:44 +05:30
Lakhan Samani
beae4502d4 feat: add integration tests for signup, login, reset_password, forgot_password, verify_email 2021-12-23 10:31:52 +05:30
Lakhan Samani
969395ccdb fix: make email verification col nullable 2021-12-22 15:38:51 +05:30
Lakhan Samani
3ee79c3937 fix: unique constraint data 2021-12-22 15:31:45 +05:30
Lakhan Samani
508c714932 fix: refactor schema for open id claim standards 2021-12-22 10:51:12 +05:30
Lakhan Samani
8f7582e1ec fix: add valid origin check for cors (#83)
Resolves #72
2021-12-21 18:46:54 +05:30
Lakhan Samani
bdbbe4adee Update README.md 2021-12-21 09:32:05 +05:30
Lakhan Samani
65478296cb feat: add mongodb support (#82)
* feat: add mongodb enum

* fix: isMongodb var condition

* feat: add init mongodb connection

* feat: add mongodb operations for various db methods

* fix: error message
2021-12-20 23:21:27 +05:30
Lakhan Samani
2342f7c5c6 build: new app 2021-12-20 18:42:02 +05:30
Lakhan Samani
8266c1cff5 chore: update authorizer-react 0.2.0 2021-12-20 18:41:48 +05:30
Lakhan Samani
c662c625a0 Update README.md 2021-12-20 18:38:17 +05:30
Lakhan Samani
c989648327 Merge branch 'main' of https://github.com/authorizerdev/authorizer 2021-12-20 18:32:46 +05:30
Lakhan Samani
a933ac1118 fix: remove count check on cursor 2021-12-20 18:32:36 +05:30
Lakhan Samani
b8afe7abcc Update README.md 2021-12-20 18:28:55 +05:30
Lakhan Samani
3ab02cc4ff Update README.md 2021-12-20 18:22:56 +05:30
Lakhan Samani
bedc3d0b50 fix: arangodb get one queries 2021-12-20 17:33:11 +05:30
Lakhan Samani
1398762e1d fix: remove completed todo 2021-12-19 05:46:14 +05:30
Lakhan Samani
e0a77da773 test: add email validtor test 2021-12-19 05:45:06 +05:30
Lakhan Samani
c3f4cd3bf9 feat: add support for sqlserver (#81)
* feat: add support for sqlserver

* fix: update gorm dependencies

* fix: update constraint
2021-12-17 21:50:57 +05:30
Lakhan Samani
f110255310 feat/add arangodb support (#80)
*feat: add arangodb init

* fix: dao for sql + arangodb

* fix: update error logs

* fix: use db name dynamically
2021-12-17 21:25:07 +05:30
Lakhan Samani
155d2e65c2 fix: use char(36) with golang uuid instead of sql uuid type (#78)
resolves #77
2021-12-14 22:57:45 +05:30
Lakhan Samani
4d341e9876 feat: add instruction for railway.app 2021-12-11 23:07:19 +05:30
Lakhan Samani
1761f41691 fix: read cookieName-client if cookie with cookieName is not present 2021-12-11 06:45:15 +05:30
Lakhan Samani
00565c8717 Fix/cookie host (#76)
* fix: cookie host

* feat: add test for url utils

* fix: url test

* fix: multi domain cookie if allowed
2021-12-11 06:41:35 +05:30
Lakhan Samani
74a551ae09 Update README.md 2021-12-09 09:17:32 +05:30
Lakhan Samani
cb5b02d777 fix: update discord link
fix: redirect link for verification handler (#74)

Resolves #70
2021-12-08 18:10:11 +05:30
Lakhan Samani
6ca37a0d50 feat: add oidc for google login 2021-12-03 22:55:27 +05:30
Lakhan Samani
a9cf301344 feat: update app dependencies 2021-11-15 04:12:28 +05:30
Lakhan Samani
0305a719db feat: add support for magic link login (#65)
* feat: add support for magic link login

* update readme
2021-11-12 05:22:03 +05:30
Lakhan Samani
4269e2242c fix: sample script 2021-10-31 11:06:07 +05:30
Lakhan Samani
71e4e35de6 add sample for custom access token script 2021-10-31 11:05:06 +05:30
Lakhan Samani
3aeb3b8d67 fix: remove log 2021-10-30 16:46:37 +05:30
Lakhan Samani
e1951bfbe0 fix: restore release 2021-10-29 21:47:28 +05:30
108 changed files with 4756 additions and 3278 deletions

View File

@@ -6,4 +6,6 @@ README.md
ROADMAP.md ROADMAP.md
build build
.env .env
data.db data.db
app/node_modules
app/build

View File

@@ -2,10 +2,15 @@ ENV=production
DATABASE_URL=data.db DATABASE_URL=data.db
DATABASE_TYPE=sqlite DATABASE_TYPE=sqlite
ADMIN_SECRET=admin ADMIN_SECRET=admin
DISABLE_EMAIL_VERIFICATION=true
JWT_SECRET=random_string JWT_SECRET=random_string
SENDER_EMAIL=info@authorizer.dev
SMTP_USERNAME=username
SMTP_PASSWORD=password
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=2525
JWT_TYPE=HS256 JWT_TYPE=HS256
ROLES=user ROLES=user
DEFAULT_ROLES=user DEFAULT_ROLES=user
PROTECTED_ROLES=admin PROTECTED_ROLES=admin
JWT_ROLE_CLAIM=role JWT_ROLE_CLAIM=role
CUSTOM_ACCESS_TOKEN_SCRIPT=function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}

View File

@@ -10,7 +10,7 @@ We're so excited you're interested in helping with Authorizer! We are happy to h
## Where to ask questions? ## Where to ask questions?
1. Check our [Github Issues](https://github.com/authorizerdev/authorizer/issues) to see if someone has already answered your question. 1. Check our [Github Issues](https://github.com/authorizerdev/authorizer/issues) to see if someone has already answered your question.
2. Join our community on [Discord](https://discord.gg/WDvCxwkX) and feel free to ask us your questions 2. Join our community on [Discord](https://discord.gg/Zv2D5h6kkK) and feel free to ask us your questions
As you gain experience with Authorizer, please help answer other people's questions! :pray: As you gain experience with Authorizer, please help answer other people's questions! :pray:
@@ -19,7 +19,7 @@ As you gain experience with Authorizer, please help answer other people's questi
You can get started by taking a look at our [Github issues](https://github.com/authorizerdev/authorizer/issues) You can get started by taking a look at our [Github issues](https://github.com/authorizerdev/authorizer/issues)
If you find one that looks interesting and no one else is already working on it, comment on that issue and start contributing 🙂. If you find one that looks interesting and no one else is already working on it, comment on that issue and start contributing 🙂.
Please ask as many questions as you need, either directly in the issue or on [Discord](https://discord.gg/WDvCxwkX). We're happy to help!:raised_hands: Please ask as many questions as you need, either directly in the issue or on [Discord](https://discord.gg/Zv2D5h6kkK). We're happy to help!:raised_hands:
### Contributions that are ALWAYS welcome ### Contributions that are ALWAYS welcome
@@ -49,3 +49,192 @@ Please ask as many questions as you need, either directly in the issue or on [Di
5. Build the code `make clean && make` 5. Build the code `make clean && make`
> Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command > Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command
6. Run binary `./build/server` 6. Run binary `./build/server`
### Testing
Make sure you test before creating PR.
If you want to test for all the databases that authorizer supports you will have to run `mongodb` & `arangodb` instances locally.
Setup mongodb & arangodb using Docker
```
docker run --name mongodb -d -p 27017:27017 mongo
docker run --name arangodb -d -p 8529:8529 -e ARANGO_ROOT_PASSWORD=root arangodb/arangodb:3.8.4
```
> Note: If you are not making any changes in db schema / db operations, you can disable those db tests [here](https://github.com/authorizerdev/authorizer/blob/main/server/__test__/resolvers_test.go#L14)
If you are adding new resolver,
1. create new resolver test file [here](https://github.com/authorizerdev/authorizer/tree/main/server/__test__)
Naming convention filename: `resolver_name_test.go` function name: `resolverNameTest(s TestSetup, t *testing.T)`
2. Add your tests [here](https://github.com/authorizerdev/authorizer/blob/main/server/__test__/resolvers_test.go#L38)
__Command to run tests:__
```sh
make test
```
__Manual Testing:__
For manually testing using graphql playground, you can paste following queries and mutations in your playground and test it
```gql
mutation Signup {
signup(params: {
email: "lakhan@yopmail.com",
password: "test",
confirm_password: "test",
given_name: "lakhan"
}) {
message
user {
id
family_name
given_name
email
email_verified
}
}
}
mutation ResendEamil {
resend_verify_email(params: {
email: "lakhan@yopmail.com"
identifier: "basic_auth_signup"
}) {
message
}
}
query GetVerifyRequests {
_verification_requests {
id
token
expires
identifier
}
}
mutation VerifyEmail {
verify_email(params: {
token: ""
}) {
access_token
expires_at
user {
id
email
given_name
email_verified
}
}
}
mutation Login {
login(params: {
email: "lakhan@yopmail.com",
password: "test"
}) {
access_token
expires_at
user {
id
family_name
given_name
email
}
}
}
query GetSession {
session {
access_token
expires_at
user {
id
given_name
family_name
email
email_verified
signup_methods
created_at
updated_at
}
}
}
mutation ForgotPassword {
forgot_password(params: {
email: "lakhan@yopmail.com"
}) {
message
}
}
mutation ResetPassword {
reset_password(params: {
token: ""
password: "test"
confirm_password: "test"
}) {
message
}
}
mutation UpdateProfile {
update_profile(params: {
family_name: "samani"
}) {
message
}
}
query GetUsers {
_users {
id
email
email_verified
given_name
family_name
picture
signup_methods
phone_number
}
}
mutation MagicLinkLogin {
magic_link_login(params: {
email: "test@yopmail.com"
}) {
message
}
}
mutation Logout {
logout {
message
}
}
mutation UpdateUser{
_update_user(params: {
id: "dafc9400-d603-4ade-997c-83fcd54bbd67",
roles: ["user", "admin"]
}) {
email
roles
}
}
mutation DeleteUser {
_delete_user(params: {
email: "signup.test134523@yopmail.com"
}) {
message
}
}
```

View File

@@ -5,26 +5,25 @@ on:
jobs: jobs:
releases: releases:
name: Release Authorizer Binary name: Release Authorizer Binary
strategy: runs-on: ubuntu-latest
matrix:
go-version: [1.16.4]
platform: [ubuntu-18.04]
runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Go - uses: actions/setup-node@v2
uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go-version }} node-version: '16'
# - name: Install dependencies - uses: actions/setup-go@v2
# run: | with:
# sudo apt-get install build-essential wget zip && \ go-version: '^1.17.3'
# go version && \ - name: Install dependencies
# wget --no-check-certificate --progress=dot:mega https://github.com/wangyoucao577/assets-uploader/releases/download/v0.3.0/github-assets-uploader-v0.3.0-linux-amd64.tar.gz -O github-assets-uploader.tar.gz && \ run: |
# tar -zxf github-assets-uploader.tar.gz && \ sudo apt-get install build-essential wget zip gcc-mingw-w64 && \
# sudo mv github-assets-uploader /usr/sbin/ && \ echo "/usr/bin/x86_64-w64-mingw32-gcc" >> GITHUB_PATH && \
# sudo rm -f github-assets-uploader.tar.gz && \ wget --no-check-certificate --progress=dot:mega https://github.com/wangyoucao577/assets-uploader/releases/download/v0.3.0/github-assets-uploader-v0.3.0-linux-amd64.tar.gz -O github-assets-uploader.tar.gz && \
# github-assets-uploader -version tar -zxf github-assets-uploader.tar.gz && \
sudo mv github-assets-uploader /usr/sbin/ && \
sudo rm -f github-assets-uploader.tar.gz && \
github-assets-uploader -version && \
make build-app
- name: Print Go paths - name: Print Go paths
run: whereis go run: whereis go
- name: Print Go Version - name: Print Go Version
@@ -44,12 +43,6 @@ jobs:
make clean && \ make clean && \
CGO_ENABLED=1 make && \ CGO_ENABLED=1 make && \
tar cvfz authorizer-${VERSION}-linux-amd64.tar.gz .env app/build build templates tar cvfz authorizer-${VERSION}-linux-amd64.tar.gz .env app/build build templates
- name: Upload asset
uses: softprops/action-gh-release@v1
with:
files: authorizer-${VERSION}-linux-amd64.tar.gz
token: ${{secrets.RELEASE_TOKEN}}
tag_name: ${VERSION}
- name: Upload assets - name: Upload assets
run: | run: |
github-assets-uploader -f authorizer-${VERSION}-windows-amd64.zip -mediatype application/zip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION} && \ github-assets-uploader -f authorizer-${VERSION}-windows-amd64.zip -mediatype application/zip -repo authorizerdev/authorizer -token ${{secrets.RELEASE_TOKEN}} -tag ${VERSION} && \

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ server/server
server/.env server/.env
data data
app/node_modules app/node_modules
app/build
build build
.env .env
data.db data.db

View File

@@ -1,5 +1,5 @@
FROM golang:1.16-alpine as builder FROM golang:1.17-alpine as go-builder
WORKDIR /app WORKDIR /authorizer
COPY server server COPY server server
COPY Makefile . COPY Makefile .
@@ -7,15 +7,22 @@ ARG VERSION="latest"
ENV VERSION="$VERSION" ENV VERSION="$VERSION"
RUN echo "$VERSION" RUN echo "$VERSION"
RUN apk add build-base nodejs &&\ RUN apk add build-base &&\
make clean && make && \ make clean && make && \
chmod 777 build/server chmod 777 build/server
FROM alpine:latest FROM node:17-alpine3.12 as node-builder
RUN apk --no-cache add ca-certificates WORKDIR /authorizer
WORKDIR /root/
COPY app app COPY app app
COPY Makefile .
RUN apk add build-base &&\
make build-app
FROM alpine:latest
WORKDIR /root/
RUN mkdir app
COPY --from=node-builder /authorizer/app/build app/build
COPY --from=go-builder /authorizer/build build
COPY templates templates COPY templates templates
COPY --from=builder /app/build build
EXPOSE 8080 EXPOSE 8080
CMD [ "./build/server" ] CMD [ "./build/server" ]

View File

@@ -2,6 +2,10 @@ DEFAULT_VERSION=0.1.0-local
VERSION := $(or $(VERSION),$(DEFAULT_VERSION)) VERSION := $(or $(VERSION),$(DEFAULT_VERSION))
cmd: cmd:
cd server && go build -ldflags "-w -X main.Version=$(VERSION)" -o '../build/server' cd server && go build -ldflags "-w -X main.VERSION=$(VERSION)" -o '../build/server'
build-app:
cd app && npm i && npm run build
clean: clean:
rm -rf build rm -rf build
test:
cd server && go clean --testcache && go test -v ./__test__

View File

@@ -7,7 +7,7 @@
Authorizer Authorizer
</h1> </h1>
**Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any SQL database. **Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any database (Currently supports [Postgres](https://www.postgresql.org/), [MySQL](https://www.mysql.com/), [SQLite](https://www.sqlite.org/index.html), [SQLServer](https://www.microsoft.com/en-us/sql-server/), [MongoDB](https://mongodb.com/),[ArangoDB](https://www.arangodb.com/)).
## Table of contents ## Table of contents
@@ -15,7 +15,7 @@
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md) - [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
- [Docs](http://docs.authorizer.dev/) - [Docs](http://docs.authorizer.dev/)
- [Join Community](https://discord.gg/2fXUQN3E) - [Join Community](https://discord.gg/Zv2D5h6kkK)
# Introduction # Introduction
@@ -30,14 +30,10 @@
- ✅ Forgot password flow using email - ✅ Forgot password flow using email
- ✅ Social logins (Google, Github, Facebook, more coming soon) - ✅ Social logins (Google, Github, Facebook, more coming soon)
- ✅ Role-based access management - ✅ Role-based access management
- ✅ Password-less login with email and magic link
## Project Status
⚠️ **Authorizer is still an early beta! missing features and bugs are to be expected!** If you can stomach it, then bring authentication and authorization to your site today!
## Roadmap ## Roadmap
- Password-less login with email and magic link
- Support more JWT encryption algorithms (Currently supporting HS256) - Support more JWT encryption algorithms (Currently supporting HS256)
- 2 Factor authentication - 2 Factor authentication
- Back office (Admin dashboard to manage user) - Back office (Admin dashboard to manage user)
@@ -70,6 +66,7 @@ This guide helps you practice using Authorizer to evaluate it before you use it
- [Install using source code](#install-using-source-code) - [Install using source code](#install-using-source-code)
- [Install using binaries](#install-using-binaries) - [Install using binaries](#install-using-binaries)
- [Install instance on heroku](#install-instance-on-Heroku) - [Install instance on heroku](#install-instance-on-Heroku)
- [Install instance on railway.app](#install-instance-on-railway)
## Install using source code ## Install using source code
@@ -138,12 +135,23 @@ Deploy Authorizer using [heroku](https://github.com/authorizerdev/authorizer-her
<br/><br/> <br/><br/>
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku) [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku)
# Install instance on railway
Deploy production ready Authorizer instance using [railway.app](https://github.com/authorizerdev/authorizer-railway) with postgres and redis for free and build with it in 30seconds
<br/>
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fauthorizerdev%2Fauthorizer-railway&plugins=postgresql%2Credis&envs=ENV%2CDATABASE_TYPE%2CADMIN_SECRET%2CCOOKIE_NAME%2CJWT_ROLE_CLAIM%2CJWT_TYPE%2CJWT_SECRET%2CFACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&optionalEnvs=FACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&ENVDesc=Deployment+environment&DATABASE_TYPEDesc=With+railway+we+are+deploying+postgres+db&ADMIN_SECRETDesc=Secret+to+access+the+admin+apis&COOKIE_NAMEDesc=Name+of+http+only+cookie+that+will+be+used+as+session&FACEBOOK_CLIENT_IDDesc=Facebook+client+ID+for+facebook+login&FACEBOOK_CLIENT_SECRETDesc=Facebook+client+secret+for+facebook+login&GOOGLE_CLIENT_IDDesc=Google+client+ID+for+google+login&GOOGLE_CLIENT_SECRETDesc=Google+client+secret+for+google+login&GITHUB_CLIENT_IDDesc=Github+client+ID+for+github+login&GITHUB_CLIENT_SECRETDesc=Github+client+secret+for+github+login&ALLOWED_ORIGINSDesc=Whitelist+the+URL+for+which+this+instance+of+authorizer+is+allowed&ROLESDesc=Comma+separated+list+of+roles+that+platform+supports.+Default+role+is+user&PROTECTED_ROLESDesc=Comma+separated+list+of+protected+roles+for+which+sign-up+is+disabled&DEFAULT_ROLESDesc=Default+role+that+should+be+assigned+to+user.+It+should+be+one+from+the+list+of+%60ROLES%60+env.+Default+role+is+user&JWT_ROLE_CLAIMDesc=JWT+key+to+be+used+to+validate+the+role+field.&JWT_TYPEDesc=JWT+encryption+type&JWT_SECRETDesc=Random+string+that+will+be+used+for+encrypting+the+JWT+token&ENVDefault=PRODUCTION&DATABASE_TYPEDefault=postgres&COOKIE_NAMEDefault=authorizer&JWT_TYPEDefault=HS256&JWT_ROLE_CLAIMDefault=role)
### Things to consider ### Things to consider
- For social logins, you will need respective social platform key and secret - For social logins, you will need respective social platform key and secret
- For having verified users, you will need an SMTP server with an email address and password using which system can send emails. The system will send a verification link to an email address. Once an email is verified then, only able to access it. - For having verified users, you will need an SMTP server with an email address and password using which system can send emails. The system will send a verification link to an email address. Once an email is verified then, only able to access it.
> Note: One can always disable the email verification to allow open sign up, which is not recommended for production as anyone can use anyone's email address 😅 > Note: One can always disable the email verification to allow open sign up, which is not recommended for production as anyone can use anyone's email address 😅
- For persisting user sessions, you will need Redis URL. If you do not configure a Redis server, sessions will be persisted until the instance is up or not restarted. For better response time on authorization requests/middleware, we recommend deploying Redis on the same infra/network as your authorizer server. - For persisting user sessions, you will need Redis URL (not in case of railway.app). If you do not configure a Redis server, sessions will be persisted until the instance is up or not restarted. For better response time on authorization requests/middleware, we recommend deploying Redis on the same infra/network as your authorizer server.
## Testing
- Check the testing instructions [here](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md#testing)
## Integrating into your website ## Integrating into your website
@@ -184,3 +192,9 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
onLoad(); onLoad();
</script> </script>
``` ```
---
### Support my work
<a href="https://www.buymeacoffee.com/lakhansamani" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

10
TODO.md
View File

@@ -1,5 +1,15 @@
# Task List # Task List
## Open ID compatible claims and schema
- [x] Rename `schema.graphqls` and re generate schema
- [x] Rename to snake case [files + schema]
- [x] Refactor db models
- [x] Check extra data in oauth profile and save accordingly
- [x] Update all the resolver to make them compatible with schema changes
- [x] Update JWT claims
- [x] Write integration tests for all resolvers
## Feature Multiple sessions ## Feature Multiple sessions
- Multiple sessions for users to login use hMset from redis for this - Multiple sessions for users to login use hMset from redis for this

View File

@@ -1,16 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #374151;
font-size: 14px;
}
*,
*:before,
*:after {
box-sizing: inherit;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11
app/esbuild.config.js Normal file
View File

@@ -0,0 +1,11 @@
const __is_prod__ = process.env.NODE_ENV === 'production';
require('esbuild').build({
entryPoints: ['src/index.tsx'],
chunkNames: '[name]-[hash]',
bundle: true,
minify: __is_prod__,
outdir: 'build',
splitting: true,
format: 'esm',
watch: !__is_prod__,
});

1006
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,20 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "esbuild src/index.tsx --bundle --minify --sourcemap --outfile=build/bundle.js" "build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js",
"start": "NODE_ENV=development node ./esbuild.config.js"
}, },
"keywords": [], "keywords": [],
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.1.0-beta.19", "@authorizerdev/authorizer-react": "latest",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-is": "^17.0.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"typescript": "^4.3.5" "typescript": "^4.3.5"
}, },

View File

@@ -1,9 +1,10 @@
import React, { useEffect } from 'react'; import React, { useEffect, lazy, Suspense } from 'react';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route } from 'react-router-dom';
import { useAuthorizer } from '@authorizerdev/authorizer-react'; import { useAuthorizer } from '@authorizerdev/authorizer-react';
import Dashboard from './pages/dashboard';
import Login from './pages/login'; const ResetPassword = lazy(() => import('./pages/rest-password'));
import ResetPassword from './pages/rest-password'; const Login = lazy(() => import('./pages/login'));
const Dashboard = lazy(() => import('./pages/dashboard'));
export default function Root() { export default function Root() {
const { token, loading, config } = useAuthorizer(); const { token, loading, config } = useAuthorizer();
@@ -24,22 +25,26 @@ export default function Root() {
if (token) { if (token) {
return ( return (
<Switch> <Suspense fallback={<></>}>
<Route path="/app" exact> <Switch>
<Dashboard /> <Route path="/app" exact>
</Route> <Dashboard />
</Switch> </Route>
</Switch>
</Suspense>
); );
} }
return ( return (
<Switch> <Suspense fallback={<></>}>
<Route path="/app" exact> <Switch>
<Login /> <Route path="/app" exact>
</Route> <Login />
<Route path="/app/reset-password"> </Route>
<ResetPassword /> <Route path="/app/reset-password">
</Route> <ResetPassword />
</Switch> </Route>
</Switch>
</Suspense>
); );
} }

16
app/src/index.css Normal file
View File

@@ -0,0 +1,16 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #374151;
font-size: 14px;
}
*,
*:before,
*:after {
box-sizing: inherit;
}

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import './index.css';
ReactDOM.render(<App />, document.getElementById('root')); ReactDOM.render(<App />, document.getElementById('root'));

View File

@@ -2,11 +2,11 @@ import React, { Fragment } from 'react';
import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react'; import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react';
export default function ResetPassword() { export default function ResetPassword() {
return ( return (
<Fragment> <Fragment>
<h1 style={{ textAlign: 'center' }}>Reset Password</h1> <h1 style={{ textAlign: 'center' }}>Reset Password</h1>
<br /> <br />
<AuthorizerResetPassword /> <AuthorizerResetPassword />
</Fragment> </Fragment>
); );
} }

View File

@@ -1,5 +1,5 @@
VERSION="$1" VERSION="$1"
make clean && CGO_ENABLED=1 VERSION=${VERSION} make make clean && make build-app && CGO_ENABLED=1 VERSION=${VERSION} make
FILE_NAME=authorizer-${VERSION}-darwin-amd64.tar.gz FILE_NAME=authorizer-${VERSION}-darwin-amd64.tar.gz
tar cvfz ${FILE_NAME} .env app/build build templates tar cvfz ${FILE_NAME} .env app/build build templates
AUTH="Authorization: token $GITHUB_TOKEN" AUTH="Authorization: token $GITHUB_TOKEN"

View File

@@ -0,0 +1,27 @@
package test
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCors(t *testing.T) {
allowedOrigin := "http://localhost:8080" // The allowed origin that you want to check
notAllowedOrigin := "http://myapp.com"
s := testSetup()
defer s.Server.Close()
client := &http.Client{}
req, _ := createContext(s)
req.Header.Add("Origin", allowedOrigin)
res, _ := client.Do(req)
// You should get your origin (or a * depending on your config) if the
// passed origin is allowed.
o := res.Header.Get("Access-Control-Allow-Origin")
assert.NotEqual(t, o, notAllowedOrigin, "Origins should not match")
assert.Equal(t, o, allowedOrigin, "Origins do match")
}

View File

@@ -0,0 +1,34 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func deleteUserTest(s TestSetup, t *testing.T) {
t.Run(`should delete users with admin secret only`, func(t *testing.T) {
req, ctx := createContext(s)
email := "delete_user." + s.TestInfo.Email
resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
_, err := resolvers.DeleteUser(ctx, model.DeleteUserInput{
Email: email,
})
assert.NotNil(t, err, "unauthorized")
req.Header.Add("x-authorizer-admin-secret", constants.ADMIN_SECRET)
_, err = resolvers.DeleteUser(ctx, model.DeleteUserInput{
Email: email,
})
assert.Nil(t, err)
cleanData(email)
})
}

View File

@@ -0,0 +1,25 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/stretchr/testify/assert"
)
func TestEnvs(t *testing.T) {
constants.ENV_PATH = "../../.env.sample"
assert.Equal(t, constants.ADMIN_SECRET, "admin")
assert.Equal(t, constants.ENV, "production")
assert.False(t, constants.DISABLE_EMAIL_VERIFICATION)
assert.False(t, constants.DISABLE_MAGIC_LINK_LOGIN)
assert.False(t, constants.DISABLE_BASIC_AUTHENTICATION)
assert.Equal(t, constants.JWT_TYPE, "HS256")
assert.Equal(t, constants.JWT_SECRET, "random_string")
assert.Equal(t, constants.JWT_ROLE_CLAIM, "role")
assert.EqualValues(t, constants.ROLES, []string{"user"})
assert.EqualValues(t, constants.DEFAULT_ROLES, []string{"user"})
assert.EqualValues(t, constants.PROTECTED_ROLES, []string{"admin"})
assert.EqualValues(t, constants.ALLOWED_ORIGINS, []string{"*"})
}

View File

@@ -0,0 +1,35 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func forgotPasswordTest(s TestSetup, t *testing.T) {
t.Run(`should run forgot password`, func(t *testing.T) {
_, ctx := createContext(s)
email := "forgot_password." + s.TestInfo.Email
_, err := resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
_, err = resolvers.ForgotPassword(ctx, model.ForgotPasswordInput{
Email: email,
})
assert.Nil(t, err, "no errors for forgot password")
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.ForgotPassword.String())
assert.Nil(t, err)
assert.Equal(t, verificationRequest.Identifier, enum.ForgotPassword.String())
cleanData(email)
})
}

View File

@@ -0,0 +1,58 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func loginTests(s TestSetup, t *testing.T) {
t.Run(`should login`, func(t *testing.T) {
_, ctx := createContext(s)
email := "login." + s.TestInfo.Email
_, err := resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
_, err = resolvers.Login(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.NotNil(t, err, "should fail because email is not verified")
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.BasicAuthSignup.String())
resolvers.VerifyEmail(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
_, err = resolvers.Login(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
Roles: []string{"test"},
})
assert.NotNil(t, err, "invalid roles")
_, err = resolvers.Login(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password + "s",
})
assert.NotNil(t, err, "invalid password")
loginRes, err := resolvers.Login(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.Nil(t, err, "login successful")
assert.NotNil(t, loginRes.AccessToken, "access token should not be empty")
cleanData(email)
})
}

View File

@@ -0,0 +1,35 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func logoutTests(s TestSetup, t *testing.T) {
t.Run(`should logout user`, func(t *testing.T) {
req, ctx := createContext(s)
email := "logout." + s.TestInfo.Email
_, err := resolvers.MagicLinkLogin(ctx, model.MagicLinkLoginInput{
Email: email,
})
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.MagicLinkLogin.String())
verifyRes, err := resolvers.VerifyEmail(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token)
_, err = resolvers.Logout(ctx)
assert.Nil(t, err)
_, err = resolvers.Profile(ctx)
assert.NotNil(t, err, "unauthorized")
cleanData(email)
})
}

View File

@@ -0,0 +1,35 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func magicLinkLoginTests(s TestSetup, t *testing.T) {
t.Run(`should login with magic link`, func(t *testing.T) {
req, ctx := createContext(s)
email := "magic_link_login." + s.TestInfo.Email
_, err := resolvers.MagicLinkLogin(ctx, model.MagicLinkLoginInput{
Email: email,
})
assert.Nil(t, err)
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.MagicLinkLogin.String())
verifyRes, err := resolvers.VerifyEmail(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token)
_, err = resolvers.Profile(ctx)
assert.Nil(t, err)
cleanData(email)
})
}

View File

@@ -0,0 +1,23 @@
package test
import (
"context"
"testing"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func metaTests(s TestSetup, t *testing.T) {
t.Run(`should get meta information`, func(t *testing.T) {
ctx := context.Background()
meta, err := resolvers.Meta(ctx)
assert.Nil(t, err)
assert.False(t, meta.IsFacebookLoginEnabled)
assert.False(t, meta.IsGoogleLoginEnabled)
assert.False(t, meta.IsGithubLoginEnabled)
assert.True(t, meta.IsEmailVerificationEnabled)
assert.True(t, meta.IsBasicAuthenticationEnabled)
assert.True(t, meta.IsMagicLinkLoginEnabled)
})
}

View File

@@ -0,0 +1,42 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func profileTests(s TestSetup, t *testing.T) {
t.Run(`should get profile only with token`, func(t *testing.T) {
req, ctx := createContext(s)
email := "profile." + s.TestInfo.Email
resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
_, err := resolvers.Profile(ctx)
assert.NotNil(t, err, "unauthorized")
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.BasicAuthSignup.String())
verifyRes, err := resolvers.VerifyEmail(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token)
profileRes, err := resolvers.Profile(ctx)
assert.Nil(t, err)
newEmail := *&profileRes.Email
assert.Equal(t, email, newEmail, "emails should be equal")
cleanData(email)
})
}

View File

@@ -0,0 +1,31 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func resendVerifyEmailTests(s TestSetup, t *testing.T) {
t.Run(`should resend verification email`, func(t *testing.T) {
_, ctx := createContext(s)
email := "resend_verify_email." + s.TestInfo.Email
_, err := resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
_, err = resolvers.ResendVerifyEmail(ctx, model.ResendVerifyEmailInput{
Email: email,
Identifier: enum.BasicAuthSignup.String(),
})
assert.Nil(t, err)
cleanData(email)
})
}

View File

@@ -0,0 +1,49 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func resetPasswordTest(s TestSetup, t *testing.T) {
t.Run(`should reset password`, func(t *testing.T) {
email := "reset_password." + s.TestInfo.Email
_, ctx := createContext(s)
_, err := resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
_, err = resolvers.ForgotPassword(ctx, model.ForgotPasswordInput{
Email: email,
})
assert.Nil(t, err, "no errors for forgot password")
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.ForgotPassword.String())
assert.Nil(t, err, "should get forgot password request")
_, err = resolvers.ResetPassword(ctx, model.ResetPasswordInput{
Token: verificationRequest.Token,
Password: "test1",
ConfirmPassword: "test",
})
assert.NotNil(t, err, "passowrds don't match")
_, err = resolvers.ResetPassword(ctx, model.ResetPasswordInput{
Token: verificationRequest.Token,
Password: "test1",
ConfirmPassword: "test1",
})
assert.Nil(t, err, "password changed successfully")
cleanData(email)
})
}

View File

@@ -0,0 +1,47 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
)
func TestResolvers(t *testing.T) {
databases := map[string]string{
enum.Sqlite.String(): "../../data.db",
enum.Arangodb.String(): "http://root:root@localhost:8529",
enum.Mongodb.String(): "mongodb://localhost:27017",
}
for dbType, dbURL := range databases {
constants.DATABASE_URL = dbURL
constants.DATABASE_TYPE = dbType
db.InitDB()
s := testSetup()
defer s.Server.Close()
t.Run("should pass tests for "+dbType, func(t *testing.T) {
loginTests(s, t)
signupTests(s, t)
forgotPasswordTest(s, t)
resendVerifyEmailTests(s, t)
resetPasswordTest(s, t)
verifyEmailTest(s, t)
sessionTests(s, t)
profileTests(s, t)
updateProfileTests(s, t)
magicLinkLoginTests(s, t)
logoutTests(s, t)
metaTests(s, t)
// admin tests
verificationRequestsTest(s, t)
usersTest(s, t)
deleteUserTest(s, t)
updateUserTest(s, t)
})
}
}

View File

@@ -0,0 +1,42 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func sessionTests(s TestSetup, t *testing.T) {
t.Run(`should allow access to profile with session only`, func(t *testing.T) {
req, ctx := createContext(s)
email := "session." + s.TestInfo.Email
resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
_, err := resolvers.Session(ctx, []string{})
assert.NotNil(t, err, "unauthorized")
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.BasicAuthSignup.String())
verifyRes, err := resolvers.VerifyEmail(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token)
sessionRes, err := resolvers.Session(ctx, []string{})
assert.Nil(t, err)
newToken := *sessionRes.AccessToken
assert.Equal(t, token, newToken, "tokens should be equal")
cleanData(email)
})
}

View File

@@ -0,0 +1,47 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func signupTests(s TestSetup, t *testing.T) {
t.Run(`should complete the signup and check duplicates`, func(t *testing.T) {
_, ctx := createContext(s)
email := "signup." + s.TestInfo.Email
res, err := resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password + "s",
})
assert.NotNil(t, err, "invalid password errors")
res, err = resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
user := *res.User
assert.Equal(t, email, user.Email)
assert.Nil(t, res.AccessToken, "access token should be nil")
res, err = resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
assert.NotNil(t, err, "should throw duplicate email error")
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.BasicAuthSignup.String())
assert.Nil(t, err)
assert.Equal(t, email, verificationRequest.Email)
cleanData(email)
})
}

95
server/__test__/test.go Normal file
View File

@@ -0,0 +1,95 @@
package test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/env"
"github.com/authorizerdev/authorizer/server/handlers"
"github.com/authorizerdev/authorizer/server/middlewares"
"github.com/authorizerdev/authorizer/server/session"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin"
)
// common user data to share across tests
type TestData struct {
Email string
Password string
}
type TestSetup struct {
GinEngine *gin.Engine
GinContext *gin.Context
Server *httptest.Server
TestInfo TestData
}
func cleanData(email string) {
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.BasicAuthSignup.String())
if err == nil {
err = db.Mgr.DeleteVerificationRequest(verificationRequest)
}
verificationRequest, err = db.Mgr.GetVerificationByEmail(email, enum.ForgotPassword.String())
if err == nil {
err = db.Mgr.DeleteVerificationRequest(verificationRequest)
}
verificationRequest, err = db.Mgr.GetVerificationByEmail(email, enum.UpdateEmail.String())
if err == nil {
err = db.Mgr.DeleteVerificationRequest(verificationRequest)
}
dbUser, err := db.Mgr.GetUserByEmail(email)
if err == nil {
db.Mgr.DeleteUser(dbUser)
db.Mgr.DeleteUserSession(dbUser.ID)
}
}
func createContext(s TestSetup) (*http.Request, context.Context) {
req, _ := http.NewRequest(
"POST",
"http://"+s.Server.Listener.Addr().String()+"/graphql",
nil,
)
ctx := context.WithValue(req.Context(), "GinContextKey", s.GinContext)
s.GinContext.Request = req
return req, ctx
}
func testSetup() TestSetup {
testData := TestData{
Email: fmt.Sprintf("%d_authorizer_tester@yopmail.com", time.Now().Unix()),
Password: "test",
}
constants.ENV_PATH = "../../.env.sample"
env.InitEnv()
session.InitSession()
w := httptest.NewRecorder()
c, r := gin.CreateTestContext(w)
r.Use(location.Default())
r.Use(middlewares.GinContextToContextMiddleware())
r.Use(middlewares.CORSMiddleware())
r.POST("/graphql", handlers.GraphqlHandler())
server := httptest.NewServer(r)
return TestSetup{
GinEngine: r,
GinContext: c,
Server: server,
TestInfo: testData,
}
}

View File

@@ -0,0 +1,53 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func updateProfileTests(s TestSetup, t *testing.T) {
t.Run(`should update the profile with access token only`, func(t *testing.T) {
req, ctx := createContext(s)
email := "update_profile." + s.TestInfo.Email
resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
fName := "samani"
_, err := resolvers.UpdateProfile(ctx, model.UpdateProfileInput{
FamilyName: &fName,
})
assert.NotNil(t, err, "unauthorized")
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.BasicAuthSignup.String())
verifyRes, err := resolvers.VerifyEmail(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token)
_, err = resolvers.UpdateProfile(ctx, model.UpdateProfileInput{
FamilyName: &fName,
})
assert.Nil(t, err)
newEmail := "new_" + email
_, err = resolvers.UpdateProfile(ctx, model.UpdateProfileInput{
Email: &newEmail,
})
assert.Nil(t, err)
_, err = resolvers.Profile(ctx)
assert.NotNil(t, err, "unauthorized")
cleanData(newEmail)
cleanData(email)
})
}

View File

@@ -0,0 +1,40 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func updateUserTest(s TestSetup, t *testing.T) {
t.Run(`should update the user with admin secret only`, func(t *testing.T) {
req, ctx := createContext(s)
email := "update_user." + s.TestInfo.Email
signupRes, _ := resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
user := *signupRes.User
adminRole := "admin"
userRole := "user"
newRoles := []*string{&adminRole, &userRole}
_, err := resolvers.UpdateUser(ctx, model.UpdateUserInput{
ID: user.ID,
Roles: newRoles,
})
assert.NotNil(t, err, "unauthorized")
req.Header.Add("x-authorizer-admin-secret", constants.ADMIN_SECRET)
_, err = resolvers.UpdateUser(ctx, model.UpdateUserInput{
ID: user.ID,
Roles: newRoles,
})
assert.Nil(t, err)
cleanData(email)
})
}

View File

@@ -0,0 +1,27 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert"
)
func TestGetHostName(t *testing.T) {
authorizer_url := "http://test.herokuapp.com:80"
host, port := utils.GetHostParts(authorizer_url)
expectedHost := "test.herokuapp.com"
assert.Equal(t, host, expectedHost, "hostname should be equal")
assert.Equal(t, port, "80", "port should be 80")
}
func TestGetDomainName(t *testing.T) {
authorizer_url := "http://test.herokuapp.com"
got := utils.GetDomainName(authorizer_url)
want := "herokuapp.com"
assert.Equal(t, got, want, "domain name should be equal")
}

View File

@@ -0,0 +1,33 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func usersTest(s TestSetup, t *testing.T) {
t.Run(`should get users list with admin secret only`, func(t *testing.T) {
req, ctx := createContext(s)
email := "users." + s.TestInfo.Email
resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
users, err := resolvers.Users(ctx)
assert.NotNil(t, err, "unauthorized")
req.Header.Add("x-authorizer-admin-secret", constants.ADMIN_SECRET)
users, err = resolvers.Users(ctx)
assert.Nil(t, err)
rLen := len(users)
assert.GreaterOrEqual(t, rLen, 1)
cleanData(email)
})
}

View File

@@ -0,0 +1,43 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert"
)
func TestIsValidEmail(t *testing.T) {
validEmail := "lakhan@gmail.com"
invalidEmail1 := "lakhan"
invalidEmail2 := "lakhan.me"
assert.True(t, utils.IsValidEmail(validEmail), "it should be valid email")
assert.False(t, utils.IsValidEmail(invalidEmail1), "it should be invalid email")
assert.False(t, utils.IsValidEmail(invalidEmail2), "it should be invalid email")
}
func TestIsValidOrigin(t *testing.T) {
// don't use portocal(http/https) for ALLOWED_ORIGINS while testing,
// as we trim them off while running the main function
constants.ALLOWED_ORIGINS = []string{"localhost:8080", "*.google.com", "*.google.in", "*abc.*"}
assert.False(t, utils.IsValidOrigin("http://myapp.com"), "it should be invalid origin")
assert.False(t, utils.IsValidOrigin("http://appgoogle.com"), "it should be invalid origin")
assert.True(t, utils.IsValidOrigin("http://app.google.com"), "it should be valid origin")
assert.False(t, utils.IsValidOrigin("http://app.google.ind"), "it should be invalid origin")
assert.True(t, utils.IsValidOrigin("http://app.google.in"), "it should be valid origin")
assert.True(t, utils.IsValidOrigin("http://xyx.abc.com"), "it should be valid origin")
assert.True(t, utils.IsValidOrigin("http://xyx.abc.in"), "it should be valid origin")
assert.True(t, utils.IsValidOrigin("http://xyxabc.in"), "it should be valid origin")
assert.True(t, utils.IsValidOrigin("http://localhost:8080"), "it should be valid origin")
}
func TestIsValidIdentifier(t *testing.T) {
assert.False(t, utils.IsValidVerificationIdentifier("test"), "it should be invalid identifier")
assert.True(t, utils.IsValidVerificationIdentifier(enum.BasicAuthSignup.String()), "it should be valid identifier")
assert.True(t, utils.IsValidVerificationIdentifier(enum.UpdateEmail.String()), "it should be valid identifier")
assert.True(t, utils.IsValidVerificationIdentifier(enum.ForgotPassword.String()), "it should be valid identifier")
}

View File

@@ -0,0 +1,35 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func verificationRequestsTest(s TestSetup, t *testing.T) {
t.Run(`should get verification requests with admin secret only`, func(t *testing.T) {
req, ctx := createContext(s)
email := "verification_requests." + s.TestInfo.Email
resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
requests, err := resolvers.VerificationRequests(ctx)
assert.NotNil(t, err, "unauthorizer")
req.Header.Add("x-authorizer-admin-secret", constants.ADMIN_SECRET)
requests, err = resolvers.VerificationRequests(ctx)
assert.Nil(t, err)
rLen := len(requests)
assert.GreaterOrEqual(t, rLen, 1)
cleanData(email)
})
}

View File

@@ -0,0 +1,38 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func verifyEmailTest(s TestSetup, t *testing.T) {
t.Run(`should verify email`, func(t *testing.T) {
_, ctx := createContext(s)
email := "verify_email." + s.TestInfo.Email
res, err := resolvers.Signup(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
user := *res.User
assert.Equal(t, email, user.Email)
assert.Nil(t, res.AccessToken, "access token should be nil")
verificationRequest, err := db.Mgr.GetVerificationByEmail(email, enum.BasicAuthSignup.String())
assert.Nil(t, err)
assert.Equal(t, email, verificationRequest.Email)
verifyRes, err := resolvers.VerifyEmail(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
assert.Nil(t, err)
assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty")
cleanData(email)
})
}

View File

@@ -1,26 +1,33 @@
package constants package constants
// this constants are configured via env
var ( var (
ADMIN_SECRET = "" ADMIN_SECRET = ""
ENV = "" ENV = ""
ENV_PATH = ""
VERSION = "" VERSION = ""
DATABASE_TYPE = "" DATABASE_TYPE = ""
DATABASE_URL = "" DATABASE_URL = ""
DATABASE_NAME = ""
SMTP_HOST = "" SMTP_HOST = ""
SMTP_PORT = "" SMTP_PORT = ""
SMTP_USERNAME = ""
SMTP_PASSWORD = ""
SENDER_EMAIL = "" SENDER_EMAIL = ""
SENDER_PASSWORD = ""
JWT_TYPE = "" JWT_TYPE = ""
JWT_SECRET = "" JWT_SECRET = ""
ALLOWED_ORIGINS = []string{} ALLOWED_ORIGINS = []string{}
AUTHORIZER_URL = "" AUTHORIZER_URL = ""
PORT = "8080" APP_URL = ""
PORT = ""
REDIS_URL = "" REDIS_URL = ""
IS_PROD = false IS_PROD = false
COOKIE_NAME = "" COOKIE_NAME = ""
RESET_PASSWORD_URL = "" RESET_PASSWORD_URL = ""
DISABLE_EMAIL_VERIFICATION = "false" DISABLE_EMAIL_VERIFICATION = false
DISABLE_BASIC_AUTHENTICATION = "false" DISABLE_BASIC_AUTHENTICATION = false
DISABLE_MAGIC_LINK_LOGIN = false
DISABLE_LOGIN_PAGE = false
// ROLES // ROLES
ROLES = []string{} ROLES = []string{}

View File

@@ -2,6 +2,7 @@ package constants
var ( var (
// Ref: https://github.com/qor/auth/blob/master/providers/google/google.go // Ref: https://github.com/qor/auth/blob/master/providers/google/google.go
// deprecated and not used. instead we follow open id approach for google login
GoogleUserInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo" GoogleUserInfoURL = "https://www.googleapis.com/oauth2/v3/userinfo"
// Ref: https://github.com/qor/auth/blob/master/providers/facebook/facebook.go#L18 // Ref: https://github.com/qor/auth/blob/master/providers/facebook/facebook.go#L18
FacebookUserInfoURL = "https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture&access_token=" FacebookUserInfoURL = "https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture&access_token="

104
server/db/arangodb.go Normal file
View File

@@ -0,0 +1,104 @@
package db
import (
"context"
"log"
"github.com/arangodb/go-driver"
arangoDriver "github.com/arangodb/go-driver"
"github.com/arangodb/go-driver/http"
"github.com/authorizerdev/authorizer/server/constants"
)
// for this we need arangodb instance up and running
// for local testing we can use dockerized version of it
// docker run -p 8529:8529 -e ARANGO_ROOT_PASSWORD=root arangodb/arangodb:3.8.4
func initArangodb() (arangoDriver.Database, error) {
ctx := context.Background()
conn, err := http.NewConnection(http.ConnectionConfig{
Endpoints: []string{constants.DATABASE_URL},
})
if err != nil {
return nil, err
}
arangoClient, err := arangoDriver.NewClient(arangoDriver.ClientConfig{
Connection: conn,
})
if err != nil {
return nil, err
}
var arangodb driver.Database
arangodb_exists, err := arangoClient.DatabaseExists(nil, constants.DATABASE_NAME)
if arangodb_exists {
log.Println(constants.DATABASE_NAME + " db exists already")
arangodb, err = arangoClient.Database(nil, constants.DATABASE_NAME)
if err != nil {
return nil, err
}
} else {
arangodb, err = arangoClient.CreateDatabase(nil, constants.DATABASE_NAME, nil)
if err != nil {
return nil, err
}
}
userCollectionExists, err := arangodb.CollectionExists(ctx, Collections.User)
if userCollectionExists {
log.Println(Collections.User + " collection exists already")
} else {
_, err = arangodb.CreateCollection(ctx, Collections.User, nil)
if err != nil {
log.Println("error creating collection("+Collections.User+"):", err)
}
}
userCollection, _ := arangodb.Collection(nil, Collections.User)
userCollection.EnsureHashIndex(ctx, []string{"email"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
userCollection.EnsureHashIndex(ctx, []string{"phone_number"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
verificationRequestCollectionExists, err := arangodb.CollectionExists(ctx, Collections.VerificationRequest)
if verificationRequestCollectionExists {
log.Println(Collections.VerificationRequest + " collection exists already")
} else {
_, err = arangodb.CreateCollection(ctx, Collections.VerificationRequest, nil)
if err != nil {
log.Println("error creating collection("+Collections.VerificationRequest+"):", err)
}
}
verificationRequestCollection, _ := arangodb.Collection(nil, Collections.VerificationRequest)
verificationRequestCollection.EnsureHashIndex(ctx, []string{"email", "identifier"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
verificationRequestCollection.EnsureHashIndex(ctx, []string{"token"}, &arangoDriver.EnsureHashIndexOptions{
Sparse: true,
})
sessionCollectionExists, err := arangodb.CollectionExists(ctx, Collections.Session)
if sessionCollectionExists {
log.Println(Collections.Session + " collection exists already")
} else {
_, err = arangodb.CreateCollection(ctx, Collections.Session, nil)
if err != nil {
log.Println("error creating collection("+Collections.Session+"):", err)
}
}
sessionCollection, _ := arangodb.Collection(nil, Collections.Session)
sessionCollection.EnsureHashIndex(ctx, []string{"user_id"}, &arangoDriver.EnsureHashIndexOptions{
Sparse: true,
})
return arangodb, err
}

View File

@@ -3,62 +3,127 @@ package db
import ( import (
"log" "log"
arangoDriver "github.com/arangodb/go-driver"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/google/uuid" "go.mongodb.org/mongo-driver/mongo"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/driver/sqlserver"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/schema" "gorm.io/gorm/schema"
) )
type Manager interface { type Manager interface {
SaveUser(user User) (User, error) AddUser(user User) (User, error)
UpdateUser(user User) (User, error) UpdateUser(user User) (User, error)
DeleteUser(user User) error
GetUsers() ([]User, error) GetUsers() ([]User, error)
GetUserByEmail(email string) (User, error) GetUserByEmail(email string) (User, error)
GetUserByID(email string) (User, error) GetUserByID(email string) (User, error)
UpdateVerificationTime(verifiedAt int64, id uuid.UUID) error
AddVerification(verification VerificationRequest) (VerificationRequest, error) AddVerification(verification VerificationRequest) (VerificationRequest, error)
GetVerificationByToken(token string) (VerificationRequest, error) GetVerificationByToken(token string) (VerificationRequest, error)
DeleteToken(email string) error DeleteVerificationRequest(verificationRequest VerificationRequest) error
GetVerificationRequests() ([]VerificationRequest, error) GetVerificationRequests() ([]VerificationRequest, error)
GetVerificationByEmail(email string) (VerificationRequest, error) GetVerificationByEmail(email string, identifier string) (VerificationRequest, error)
DeleteUser(email string) error AddSession(session Session) error
SaveRoles(roles []Role) error DeleteUserSession(userId string) error
SaveSession(session Session) error
} }
type manager struct { type manager struct {
db *gorm.DB sqlDB *gorm.DB
arangodb arangoDriver.Database
mongodb *mongo.Database
} }
var Mgr Manager // mainly used by nosql dbs
type CollectionList struct {
User string
VerificationRequest string
Session string
}
var (
IsORMSupported bool
IsArangoDB bool
IsMongoDB bool
Mgr Manager
Prefix = "authorizer_"
Collections = CollectionList{
User: Prefix + "users",
VerificationRequest: Prefix + "verification_requests",
Session: Prefix + "sessions",
}
)
func InitDB() { func InitDB() {
var db *gorm.DB var sqlDB *gorm.DB
var err error var err error
IsORMSupported = constants.DATABASE_TYPE != enum.Arangodb.String() && constants.DATABASE_TYPE != enum.Mongodb.String()
IsArangoDB = constants.DATABASE_TYPE == enum.Arangodb.String()
IsMongoDB = constants.DATABASE_TYPE == enum.Mongodb.String()
// sql db orm config
ormConfig := &gorm.Config{ ormConfig := &gorm.Config{
NamingStrategy: schema.NamingStrategy{ NamingStrategy: schema.NamingStrategy{
TablePrefix: "authorizer_", TablePrefix: Prefix,
}, },
} }
if constants.DATABASE_TYPE == enum.Postgres.String() {
db, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig) log.Println("db type:", constants.DATABASE_TYPE)
}
if constants.DATABASE_TYPE == enum.Mysql.String() { switch constants.DATABASE_TYPE {
db, err = gorm.Open(mysql.Open(constants.DATABASE_URL), ormConfig) case enum.Postgres.String():
} sqlDB, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig)
if constants.DATABASE_TYPE == enum.Sqlite.String() { break
db, err = gorm.Open(sqlite.Open(constants.DATABASE_URL), ormConfig) case enum.Sqlite.String():
sqlDB, err = gorm.Open(sqlite.Open(constants.DATABASE_URL), ormConfig)
break
case enum.Mysql.String():
sqlDB, err = gorm.Open(mysql.Open(constants.DATABASE_URL), ormConfig)
break
case enum.SQLServer.String():
sqlDB, err = gorm.Open(sqlserver.Open(constants.DATABASE_URL), ormConfig)
break
case enum.Arangodb.String():
arangodb, err := initArangodb()
if err != nil {
log.Fatal("error initializing arangodb:", err)
}
Mgr = &manager{
sqlDB: nil,
arangodb: arangodb,
mongodb: nil,
}
break
case enum.Mongodb.String():
mongodb, err := initMongodb()
if err != nil {
log.Fatal("error initializing mongodb connection:", err)
}
Mgr = &manager{
sqlDB: nil,
arangodb: nil,
mongodb: mongodb,
}
} }
if err != nil { // common for all sql dbs that are configured via go-orm
log.Fatal("Failed to init db:", err) if IsORMSupported {
} else { if err != nil {
db.AutoMigrate(&User{}, &VerificationRequest{}, &Role{}, &Session{}) log.Fatal("Failed to init sqlDB:", err)
} else {
sqlDB.AutoMigrate(&User{}, &VerificationRequest{}, &Session{})
}
Mgr = &manager{
sqlDB: sqlDB,
arangodb: nil,
mongodb: nil,
}
} }
Mgr = &manager{db: db}
} }

77
server/db/mongodb.go Normal file
View File

@@ -0,0 +1,77 @@
package db
import (
"context"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
func initMongodb() (*mongo.Database, error) {
mongodbOptions := options.Client().ApplyURI(constants.DATABASE_URL)
maxWait := time.Duration(5 * time.Second)
mongodbOptions.ConnectTimeout = &maxWait
mongoClient, err := mongo.NewClient(mongodbOptions)
if err != nil {
return nil, err
}
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
err = mongoClient.Connect(ctx)
if err != nil {
return nil, err
}
err = mongoClient.Ping(ctx, readpref.Primary())
if err != nil {
return nil, err
}
mongodb := mongoClient.Database(constants.DATABASE_NAME, options.Database())
mongodb.CreateCollection(ctx, Collections.User, options.CreateCollection())
userCollection := mongodb.Collection(Collections.User, options.Collection())
userCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"email": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
userCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"phone_number": 1},
Options: options.Index().SetUnique(true).SetSparse(true).SetPartialFilterExpression(map[string]interface{}{
"phone_number": map[string]string{"$type": "string"},
}),
},
}, options.CreateIndexes())
mongodb.CreateCollection(ctx, Collections.VerificationRequest, options.CreateCollection())
verificationRequestCollection := mongodb.Collection(Collections.VerificationRequest, options.Collection())
verificationRequestCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"email": 1, "identifier": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
verificationRequestCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"token": 1},
Options: options.Index().SetSparse(true),
},
}, options.CreateIndexes())
mongodb.CreateCollection(ctx, Collections.Session, options.CreateCollection())
sessionCollection := mongodb.Collection(Collections.Session, options.Collection())
sessionCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
mongo.IndexModel{
Keys: bson.M{"user_id": 1},
Options: options.Index().SetSparse(true),
},
}, options.CreateIndexes())
return mongodb, nil
}

View File

@@ -1,34 +0,0 @@
package db
import (
"log"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Role struct {
ID uuid.UUID `gorm:"type:uuid;"`
Role string `gorm:"unique"`
}
func (r *Role) BeforeCreate(tx *gorm.DB) (err error) {
r.ID = uuid.New()
return
}
// SaveRoles function to save roles
func (mgr *manager) SaveRoles(roles []Role) error {
res := mgr.db.Clauses(
clause.OnConflict{
DoNothing: true,
}).Create(&roles)
if res.Error != nil {
log.Println(`Error saving roles`)
return res.Error
}
return nil
}

View File

@@ -1,38 +1,101 @@
package db package db
import ( import (
"fmt"
"log" "log"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
type Session struct { type Session struct {
ID uuid.UUID `gorm:"type:uuid;"` Key string `json:"_key,omitempty" bson:"_key,omitempty"` // for arangodb
UserID uuid.UUID `gorm:"type:uuid;"` ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
User User UserID string `gorm:"type:char(36),index:" json:"user_id" bson:"user_id"`
UserAgent string User User `json:"-" bson:"-"`
IP string UserAgent string `json:"user_agent" bson:"user_agent"`
CreatedAt int64 `gorm:"autoCreateTime"` IP string `json:"ip" bson:"ip"`
UpdatedAt int64 `gorm:"autoUpdateTime"` CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
} }
func (r *Session) BeforeCreate(tx *gorm.DB) (err error) { // AddSession function to save user sessiosn
r.ID = uuid.New() func (mgr *manager) AddSession(session Session) error {
if session.ID == "" {
session.ID = uuid.New().String()
}
return if IsORMSupported {
} session.Key = session.ID
res := mgr.sqlDB.Clauses(
clause.OnConflict{
DoNothing: true,
}).Create(&session)
if res.Error != nil {
log.Println(`error saving session`, res.Error)
return res.Error
}
}
// SaveSession function to save user sessiosn if IsArangoDB {
func (mgr *manager) SaveSession(session Session) error { session.CreatedAt = time.Now().Unix()
res := mgr.db.Clauses( session.UpdatedAt = time.Now().Unix()
clause.OnConflict{ sessionCollection, _ := mgr.arangodb.Collection(nil, Collections.Session)
DoNothing: true, _, err := sessionCollection.CreateDocument(nil, session)
}).Create(&session) if err != nil {
if res.Error != nil { log.Println(`error saving session`, err)
log.Println(`Error saving session`, res.Error) return err
return res.Error }
}
if IsMongoDB {
session.Key = session.ID
session.CreatedAt = time.Now().Unix()
session.UpdatedAt = time.Now().Unix()
sessionCollection := mgr.mongodb.Collection(Collections.Session, options.Collection())
_, err := sessionCollection.InsertOne(nil, session)
if err != nil {
log.Println(`error saving session`, err)
return err
}
}
return nil
}
func (mgr *manager) DeleteUserSession(userId string) error {
if IsORMSupported {
result := mgr.sqlDB.Where("user_id = ?", userId).Delete(&Session{})
if result.Error != nil {
log.Println(`error deleting session:`, result.Error)
return result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf(`FOR d IN %s FILTER d.user_id == @userId REMOVE { _key: d._key } IN %s`, Collections.Session, Collections.Session)
bindVars := map[string]interface{}{
"userId": userId,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
log.Println("=> error deleting arangodb session:", err)
return err
}
defer cursor.Close()
}
if IsMongoDB {
sessionCollection := mgr.mongodb.Collection(Collections.Session, options.Collection())
_, err := sessionCollection.DeleteMany(nil, bson.M{"user_id": userId}, options.Delete())
if err != nil {
log.Println("error deleting session:", err)
return err
}
} }
return nil return nil

View File

@@ -1,82 +1,233 @@
package db package db
import ( import (
"fmt"
"log" "log"
"time" "time"
"github.com/arangodb/go-driver"
arangoDriver "github.com/arangodb/go-driver"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
type User struct { type User struct {
ID uuid.UUID `gorm:"type:uuid;"` Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
FirstName string ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
LastName string
Email string `gorm:"unique"` Email string `gorm:"unique" json:"email" bson:"email"`
Password string EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at"`
SignupMethod string Password *string `gorm:"type:text" json:"password" bson:"password"`
EmailVerifiedAt int64 SignupMethods string `json:"signup_methods" bson:"signup_methods"`
CreatedAt int64 `gorm:"autoCreateTime"` GivenName *string `json:"given_name" bson:"given_name"`
UpdatedAt int64 `gorm:"autoUpdateTime"` FamilyName *string `json:"family_name" bson:"family_name"`
Image string MiddleName *string `json:"middle_name" bson:"middle_name"`
Roles string Nickname *string `json:"nickname" bson:"nickname"`
Gender *string `json:"gender" bson:"gender"`
Birthdate *string `json:"birthdate" bson:"birthdate"`
PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number"`
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at"`
Picture *string `gorm:"type:text" json:"picture" bson:"picture"`
Roles string `json:"roles" bson:"roles"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
} }
func (u *User) BeforeCreate(tx *gorm.DB) (err error) { // AddUser function to add user even with email conflict
u.ID = uuid.New() func (mgr *manager) AddUser(user User) (User, error) {
if user.ID == "" {
return user.ID = uuid.New().String()
}
// SaveUser function to add user even with email conflict
func (mgr *manager) SaveUser(user User) (User, error) {
result := mgr.db.Clauses(
clause.OnConflict{
UpdateAll: true,
Columns: []clause.Column{{Name: "email"}},
}).Create(&user)
if result.Error != nil {
log.Println(result.Error)
return user, result.Error
} }
if user.Roles == "" {
user.Roles = constants.DEFAULT_ROLES[0]
}
if IsORMSupported {
// copy id as value for fields required for mongodb & arangodb
user.Key = user.ID
result := mgr.sqlDB.Clauses(
clause.OnConflict{
UpdateAll: true,
Columns: []clause.Column{{Name: "email"}},
}).Create(&user)
if result.Error != nil {
log.Println("error adding user:", result.Error)
return user, result.Error
}
}
if IsArangoDB {
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
userCollection, _ := mgr.arangodb.Collection(nil, Collections.User)
meta, err := userCollection.CreateDocument(arangoDriver.WithOverwrite(nil), user)
if err != nil {
log.Println("error adding user:", err)
return user, err
}
user.Key = meta.Key
user.ID = meta.ID.String()
}
if IsMongoDB {
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
user.Key = user.ID
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
_, err := userCollection.InsertOne(nil, user)
if err != nil {
log.Println("error adding user:", err)
return user, err
}
}
return user, nil return user, nil
} }
// UpdateUser function to update user with ID conflict // UpdateUser function to update user with ID conflict
func (mgr *manager) UpdateUser(user User) (User, error) { func (mgr *manager) UpdateUser(user User) (User, error) {
user.UpdatedAt = time.Now().Unix() user.UpdatedAt = time.Now().Unix()
result := mgr.db.Clauses(
clause.OnConflict{
UpdateAll: true,
Columns: []clause.Column{{Name: "email"}},
}).Create(&user)
if result.Error != nil { if IsORMSupported {
log.Println(result.Error) result := mgr.sqlDB.Save(&user)
return user, result.Error
if result.Error != nil {
log.Println("error updating user:", result.Error)
return user, result.Error
}
} }
if IsArangoDB {
collection, _ := mgr.arangodb.Collection(nil, Collections.User)
meta, err := collection.UpdateDocument(nil, user.Key, user)
if err != nil {
log.Println("error updating user:", err)
return user, err
}
user.Key = meta.Key
user.ID = meta.ID.String()
}
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
_, err := userCollection.UpdateOne(nil, bson.M{"_id": bson.M{"$eq": user.ID}}, bson.M{"$set": user}, options.MergeUpdateOptions())
if err != nil {
log.Println("error updating user:", err)
return user, err
}
}
return user, nil return user, nil
} }
// GetUsers function to get all users // GetUsers function to get all users
func (mgr *manager) GetUsers() ([]User, error) { func (mgr *manager) GetUsers() ([]User, error) {
var users []User var users []User
result := mgr.db.Find(&users)
if result.Error != nil { if IsORMSupported {
log.Println(result.Error) result := mgr.sqlDB.Find(&users)
return users, result.Error if result.Error != nil {
log.Println("error getting users:", result.Error)
return users, result.Error
}
} }
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s RETURN d", Collections.User)
cursor, err := mgr.arangodb.Query(nil, query, nil)
if err != nil {
return users, err
}
defer cursor.Close()
for {
var user User
meta, err := cursor.ReadDocument(nil, &user)
if driver.IsNoMoreDocuments(err) {
break
} else if err != nil {
return users, err
}
if meta.Key != "" {
users = append(users, user)
}
}
}
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
cursor, err := userCollection.Find(nil, bson.M{}, options.Find())
if err != nil {
log.Println("error getting users:", err)
return users, err
}
defer cursor.Close(nil)
for cursor.Next(nil) {
var user User
err := cursor.Decode(&user)
if err != nil {
return users, err
}
users = append(users, user)
}
}
return users, nil return users, nil
} }
func (mgr *manager) GetUserByEmail(email string) (User, error) { func (mgr *manager) GetUserByEmail(email string) (User, error) {
var user User var user User
result := mgr.db.Where("email = ?", email).First(&user)
if result.Error != nil { if IsORMSupported {
return user, result.Error result := mgr.sqlDB.Where("email = ?", email).First(&user)
if result.Error != nil {
return user, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", Collections.User)
bindVars := map[string]interface{}{
"email": email,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
return user, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if user.Key == "" {
return user, fmt.Errorf("user not found")
}
break
}
_, err := cursor.ReadDocument(nil, &user)
if err != nil {
return user, err
}
}
}
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
err := userCollection.FindOne(nil, bson.M{"email": email}).Decode(&user)
if err != nil {
return user, err
}
} }
return user, nil return user, nil
@@ -84,35 +235,78 @@ func (mgr *manager) GetUserByEmail(email string) (User, error) {
func (mgr *manager) GetUserByID(id string) (User, error) { func (mgr *manager) GetUserByID(id string) (User, error) {
var user User var user User
result := mgr.db.Where("id = ?", id).First(&user)
if result.Error != nil { if IsORMSupported {
return user, result.Error result := mgr.sqlDB.Where("id = ?", id).First(&user)
if result.Error != nil {
return user, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s FILTER d._id == @id LIMIT 1 RETURN d", Collections.User)
bindVars := map[string]interface{}{
"id": id,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
return user, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if user.Key == "" {
return user, fmt.Errorf("user not found")
}
break
}
_, err := cursor.ReadDocument(nil, &user)
if err != nil {
return user, err
}
}
}
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
err := userCollection.FindOne(nil, bson.M{"_id": id}).Decode(&user)
if err != nil {
return user, err
}
} }
return user, nil return user, nil
} }
func (mgr *manager) UpdateVerificationTime(verifiedAt int64, id uuid.UUID) error { func (mgr *manager) DeleteUser(user User) error {
user := &User{ if IsORMSupported {
ID: id, result := mgr.sqlDB.Delete(&user)
}
result := mgr.db.Model(&user).Where("id = ?", id).Update("email_verified_at", verifiedAt)
if result.Error != nil { if result.Error != nil {
return result.Error log.Println(`error deleting user:`, result.Error)
} return result.Error
}
return nil }
}
if IsArangoDB {
func (mgr *manager) DeleteUser(email string) error { collection, _ := mgr.arangodb.Collection(nil, Collections.User)
var user User _, err := collection.RemoveDocument(nil, user.Key)
result := mgr.db.Where("email = ?", email).Delete(&user) if err != nil {
log.Println(`error deleting user:`, err)
if result.Error != nil { return err
log.Println(`Error deleting user:`, result.Error) }
return result.Error }
if IsMongoDB {
userCollection := mgr.mongodb.Collection(Collections.User, options.Collection())
_, err := userCollection.DeleteOne(nil, bson.M{"_id": user.ID}, options.Delete())
if err != nil {
log.Println("error deleting user:", err)
return err
}
} }
return nil return nil

View File

@@ -1,86 +0,0 @@
package db
import (
"log"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type VerificationRequest struct {
ID uuid.UUID `gorm:"type:uuid;"`
Token string `gorm:"index"`
Identifier string
ExpiresAt int64
CreatedAt int64 `gorm:"autoCreateTime"`
UpdatedAt int64 `gorm:"autoUpdateTime"`
Email string `gorm:"unique"`
}
func (v *VerificationRequest) BeforeCreate(tx *gorm.DB) (err error) {
v.ID = uuid.New()
return
}
// AddVerification function to add verification record
func (mgr *manager) AddVerification(verification VerificationRequest) (VerificationRequest, error) {
result := mgr.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
DoUpdates: clause.AssignmentColumns([]string{"token", "identifier", "expires_at"}),
}).Create(&verification)
if result.Error != nil {
log.Println(`Error saving verification record`, result.Error)
return verification, result.Error
}
return verification, nil
}
func (mgr *manager) GetVerificationByToken(token string) (VerificationRequest, error) {
var verification VerificationRequest
result := mgr.db.Where("token = ?", token).First(&verification)
if result.Error != nil {
log.Println(`Error getting verification token:`, result.Error)
return verification, result.Error
}
return verification, nil
}
func (mgr *manager) GetVerificationByEmail(email string) (VerificationRequest, error) {
var verification VerificationRequest
result := mgr.db.Where("email = ?", email).First(&verification)
if result.Error != nil {
log.Println(`Error getting verification token:`, result.Error)
return verification, result.Error
}
return verification, nil
}
func (mgr *manager) DeleteToken(email string) error {
var verification VerificationRequest
result := mgr.db.Where("email = ?", email).Delete(&verification)
if result.Error != nil {
log.Println(`Error deleting token:`, result.Error)
return result.Error
}
return nil
}
// GetUsers function to get all users
func (mgr *manager) GetVerificationRequests() ([]VerificationRequest, error) {
var verificationRequests []VerificationRequest
result := mgr.db.Find(&verificationRequests)
if result.Error != nil {
log.Println(result.Error)
return verificationRequests, result.Error
}
return verificationRequests, nil
}

View File

@@ -0,0 +1,260 @@
package db
import (
"fmt"
"log"
"time"
"github.com/arangodb/go-driver"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"gorm.io/gorm/clause"
)
type VerificationRequest struct {
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
Token string `gorm:"type:text" json:"token" bson:"token"`
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
}
// AddVerification function to add verification record
func (mgr *manager) AddVerification(verification VerificationRequest) (VerificationRequest, error) {
if verification.ID == "" {
verification.ID = uuid.New().String()
}
if IsORMSupported {
// copy id as value for fields required for mongodb & arangodb
verification.Key = verification.ID
result := mgr.sqlDB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}),
}).Create(&verification)
if result.Error != nil {
log.Println(`error saving verification record`, result.Error)
return verification, result.Error
}
}
if IsArangoDB {
verification.CreatedAt = time.Now().Unix()
verification.UpdatedAt = time.Now().Unix()
verificationRequestCollection, _ := mgr.arangodb.Collection(nil, Collections.VerificationRequest)
meta, err := verificationRequestCollection.CreateDocument(nil, verification)
if err != nil {
log.Println("error saving verification record:", err)
return verification, err
}
verification.Key = meta.Key
verification.ID = meta.ID.String()
}
if IsMongoDB {
verification.CreatedAt = time.Now().Unix()
verification.UpdatedAt = time.Now().Unix()
verification.Key = verification.ID
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
_, err := verificationRequestCollection.InsertOne(nil, verification)
if err != nil {
log.Println("error saving verification record:", err)
return verification, err
}
}
return verification, nil
}
// GetVerificationRequests function to get all verification requests
func (mgr *manager) GetVerificationRequests() ([]VerificationRequest, error) {
var verificationRequests []VerificationRequest
if IsORMSupported {
result := mgr.sqlDB.Find(&verificationRequests)
if result.Error != nil {
log.Println("error getting verification requests:", result.Error)
return verificationRequests, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s RETURN d", Collections.VerificationRequest)
cursor, err := mgr.arangodb.Query(nil, query, nil)
if err != nil {
return verificationRequests, err
}
defer cursor.Close()
for {
var verificationRequest VerificationRequest
meta, err := cursor.ReadDocument(nil, &verificationRequest)
if driver.IsNoMoreDocuments(err) {
break
} else if err != nil {
return verificationRequests, err
}
if meta.Key != "" {
verificationRequests = append(verificationRequests, verificationRequest)
}
}
}
if IsMongoDB {
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
cursor, err := verificationRequestCollection.Find(nil, bson.M{}, options.Find())
if err != nil {
log.Println("error getting verification requests:", err)
return verificationRequests, err
}
defer cursor.Close(nil)
for cursor.Next(nil) {
var verificationRequest VerificationRequest
err := cursor.Decode(&verificationRequest)
if err != nil {
return verificationRequests, err
}
verificationRequests = append(verificationRequests, verificationRequest)
}
}
return verificationRequests, nil
}
func (mgr *manager) GetVerificationByToken(token string) (VerificationRequest, error) {
var verification VerificationRequest
if IsORMSupported {
result := mgr.sqlDB.Where("token = ?", token).First(&verification)
if result.Error != nil {
log.Println(`error getting verification request:`, result.Error)
return verification, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s FILTER d.token == @token LIMIT 1 RETURN d", Collections.VerificationRequest)
bindVars := map[string]interface{}{
"token": token,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
return verification, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if verification.Key == "" {
return verification, fmt.Errorf("verification request not found")
}
break
}
_, err := cursor.ReadDocument(nil, &verification)
if err != nil {
return verification, err
}
}
}
if IsMongoDB {
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
err := verificationRequestCollection.FindOne(nil, bson.M{"token": token}).Decode(&verification)
if err != nil {
return verification, err
}
}
return verification, nil
}
func (mgr *manager) GetVerificationByEmail(email string, identifier string) (VerificationRequest, error) {
var verification VerificationRequest
if IsORMSupported {
result := mgr.sqlDB.Where("email = ? AND identifier = ?", email, identifier).First(&verification)
if result.Error != nil {
log.Println(`error getting verification token:`, result.Error)
return verification, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email FILTER d.identifier == @identifier LIMIT 1 RETURN d", Collections.VerificationRequest)
bindVars := map[string]interface{}{
"email": email,
"identifier": identifier,
}
cursor, err := mgr.arangodb.Query(nil, query, bindVars)
if err != nil {
return verification, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if verification.Key == "" {
return verification, fmt.Errorf("verification request not found")
}
break
}
_, err := cursor.ReadDocument(nil, &verification)
if err != nil {
return verification, err
}
}
}
if IsMongoDB {
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
err := verificationRequestCollection.FindOne(nil, bson.M{"email": email, "identifier": identifier}).Decode(&verification)
if err != nil {
return verification, err
}
}
return verification, nil
}
func (mgr *manager) DeleteVerificationRequest(verificationRequest VerificationRequest) error {
if IsORMSupported {
result := mgr.sqlDB.Delete(&verificationRequest)
if result.Error != nil {
log.Println(`error deleting verification request:`, result.Error)
return result.Error
}
}
if IsArangoDB {
collection, _ := mgr.arangodb.Collection(nil, Collections.VerificationRequest)
_, err := collection.RemoveDocument(nil, verificationRequest.Key)
if err != nil {
log.Println(`error deleting verification request:`, err)
return err
}
}
if IsMongoDB {
verificationRequestCollection := mgr.mongodb.Collection(Collections.VerificationRequest, options.Collection())
_, err := verificationRequestCollection.DeleteOne(nil, bson.M{"_id": verificationRequest.ID}, options.Delete())
if err != nil {
log.Println("error deleting verification request::", err)
return err
}
}
return nil
}

View File

@@ -1,90 +1,28 @@
package email package email
import ( import (
"bytes" "crypto/tls"
"fmt"
"log" "log"
"mime/quotedprintable" "strconv"
"net/smtp"
"strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
gomail "gopkg.in/mail.v2"
) )
/** func SendMail(to []string, Subject, bodyMessage string) error {
Using: https://github.com/tangingw/go_smtp/blob/master/send_mail.go m := gomail.NewMessage()
For gmail add instruction to enable less security m.SetHeader("From", constants.SENDER_EMAIL)
// https://myaccount.google.com/u/0/lesssecureapps m.SetHeader("To", to...)
// https://www.google.com/settings/security/lesssecureapps m.SetHeader("Subject", Subject)
// https://stackoverflow.com/questions/19877246/nodemailer-with-gmail-and-nodejs m.SetBody("text/html", bodyMessage)
**/ port, _ := strconv.Atoi(constants.SMTP_PORT)
d := gomail.NewDialer(constants.SMTP_HOST, port, constants.SMTP_USERNAME, constants.SMTP_PASSWORD)
// TODO -> try using gomail.v2 if constants.ENV == "development" {
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
type Sender struct { }
User string if err := d.DialAndSend(m); err != nil {
Password string
}
func NewSender() Sender {
return Sender{User: constants.SENDER_EMAIL, Password: constants.SENDER_PASSWORD}
}
func (sender Sender) SendMail(Dest []string, Subject, bodyMessage string) error {
msg := "From: " + sender.User + "\n" +
"To: " + strings.Join(Dest, ",") + "\n" +
"Subject: " + Subject + "\n" + bodyMessage
err := smtp.SendMail(constants.SMTP_HOST+":"+constants.SMTP_PORT,
smtp.PlainAuth("", sender.User, sender.Password, constants.SMTP_HOST),
sender.User, Dest, []byte(msg))
if err != nil {
log.Printf("smtp error: %s", err) log.Printf("smtp error: %s", err)
return err return err
} }
return nil return nil
} }
func (sender Sender) WriteEmail(dest []string, contentType, subject, bodyMessage string) string {
header := make(map[string]string)
header["From"] = sender.User
receipient := ""
for _, user := range dest {
receipient = receipient + user
}
header["To"] = receipient
header["Subject"] = subject
header["MIME-Version"] = "1.0"
header["Content-Type"] = fmt.Sprintf("%s; charset=\"utf-8\"", contentType)
header["Content-Transfer-Encoding"] = "quoted-printable"
header["Content-Disposition"] = "inline"
message := ""
for key, value := range header {
message += fmt.Sprintf("%s: %s\r\n", key, value)
}
var encodedMessage bytes.Buffer
finalMessage := quotedprintable.NewWriter(&encodedMessage)
finalMessage.Write([]byte(bodyMessage))
finalMessage.Close()
message += "\r\n" + encodedMessage.String()
return message
}
func (sender *Sender) WriteHTMLEmail(dest []string, subject, bodyMessage string) string {
return sender.WriteEmail(dest, "text/html", subject, bodyMessage)
}
func (sender *Sender) WritePlainEmail(dest []string, subject, bodyMessage string) string {
return sender.WriteEmail(dest, "text/plain", subject, bodyMessage)
}

View File

@@ -6,6 +6,9 @@ const (
Postgres DbType = iota Postgres DbType = iota
Sqlite Sqlite
Mysql Mysql
SQLServer
Arangodb
Mongodb
) )
func (d DbType) String() string { func (d DbType) String() string {
@@ -13,5 +16,8 @@ func (d DbType) String() string {
"postgres", "postgres",
"sqlite", "sqlite",
"mysql", "mysql",
"sqlserver",
"arangodb",
"mongodb",
}[d] }[d]
} }

View File

@@ -4,7 +4,7 @@ type SignupMethod int
const ( const (
BasicAuth SignupMethod = iota BasicAuth SignupMethod = iota
MagicLink MagicLinkLogin
Google Google
Github Github
Facebook Facebook
@@ -13,7 +13,7 @@ const (
func (d SignupMethod) String() string { func (d SignupMethod) String() string {
return [...]string{ return [...]string{
"basic_auth", "basic_auth",
"magic_link", "magic_link_login",
"google", "google",
"github", "github",
"facebook", "facebook",

View File

@@ -1,190 +0,0 @@
package main
import (
"flag"
"log"
"os"
"strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/joho/godotenv"
)
// build variables
var (
Version string
ARG_DB_URL *string
ARG_DB_TYPE *string
ARG_AUTHORIZER_URL *string
ARG_ENV_FILE *string
)
// InitEnv -> to initialize env and through error if required env are not present
func InitEnv() {
envPath := `.env`
ARG_DB_URL = flag.String("database_url", "", "Database connection string")
ARG_DB_TYPE = flag.String("database_type", "", "Database type, possible values are postgres,mysql,sqlite")
ARG_AUTHORIZER_URL = flag.String("authorizer_url", "", "URL for authorizer instance, eg: https://xyz.herokuapp.com")
ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
flag.Parse()
if *ARG_ENV_FILE != "" {
envPath = *ARG_ENV_FILE
}
err := godotenv.Load(envPath)
if err != nil {
log.Println("Error loading .env file")
}
constants.VERSION = Version
constants.ADMIN_SECRET = os.Getenv("ADMIN_SECRET")
constants.ENV = os.Getenv("ENV")
constants.DATABASE_TYPE = os.Getenv("DATABASE_TYPE")
constants.DATABASE_URL = os.Getenv("DATABASE_URL")
constants.SMTP_HOST = os.Getenv("SMTP_HOST")
constants.SMTP_PORT = os.Getenv("SMTP_PORT")
constants.SENDER_EMAIL = os.Getenv("SENDER_EMAIL")
constants.SENDER_PASSWORD = os.Getenv("SENDER_PASSWORD")
constants.JWT_SECRET = os.Getenv("JWT_SECRET")
constants.JWT_TYPE = os.Getenv("JWT_TYPE")
constants.AUTHORIZER_URL = strings.TrimSuffix(os.Getenv("AUTHORIZER_URL"), "/")
constants.PORT = os.Getenv("PORT")
constants.REDIS_URL = os.Getenv("REDIS_URL")
constants.COOKIE_NAME = os.Getenv("COOKIE_NAME")
constants.GOOGLE_CLIENT_ID = os.Getenv("GOOGLE_CLIENT_ID")
constants.GOOGLE_CLIENT_SECRET = os.Getenv("GOOGLE_CLIENT_SECRET")
constants.GITHUB_CLIENT_ID = os.Getenv("GITHUB_CLIENT_ID")
constants.GITHUB_CLIENT_SECRET = os.Getenv("GITHUB_CLIENT_SECRET")
constants.FACEBOOK_CLIENT_ID = os.Getenv("FACEBOOK_CLIENT_ID")
constants.FACEBOOK_CLIENT_SECRET = os.Getenv("FACEBOOK_CLIENT_SECRET")
constants.TWITTER_CLIENT_ID = os.Getenv("TWITTER_CLIENT_ID")
constants.TWITTER_CLIENT_SECRET = os.Getenv("TWITTER_CLIENT_SECRET")
constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/")
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION")
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION")
constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM")
if constants.ADMIN_SECRET == "" {
panic("root admin secret is required")
}
if constants.ENV == "" {
constants.ENV = "production"
}
if constants.ENV == "production" {
constants.IS_PROD = true
os.Setenv("GIN_MODE", "release")
} else {
constants.IS_PROD = false
}
allowedOriginsSplit := strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",")
allowedOrigins := []string{}
for _, val := range allowedOriginsSplit {
trimVal := strings.TrimSpace(val)
if trimVal != "" {
allowedOrigins = append(allowedOrigins, trimVal)
}
}
if len(allowedOrigins) == 0 {
allowedOrigins = []string{"*"}
}
constants.ALLOWED_ORIGINS = allowedOrigins
if *ARG_AUTHORIZER_URL != "" {
constants.AUTHORIZER_URL = *ARG_AUTHORIZER_URL
}
if *ARG_DB_URL != "" {
constants.DATABASE_URL = *ARG_DB_URL
}
if *ARG_DB_TYPE != "" {
constants.DATABASE_TYPE = *ARG_DB_TYPE
}
if constants.DATABASE_URL == "" {
panic("Database url is required")
}
if constants.DATABASE_TYPE == "" {
panic("Database type is required")
}
if constants.JWT_TYPE == "" {
constants.JWT_TYPE = "HS256"
}
if constants.COOKIE_NAME == "" {
constants.COOKIE_NAME = "authorizer"
}
if constants.DISABLE_BASIC_AUTHENTICATION == "" {
constants.DISABLE_BASIC_AUTHENTICATION = "false"
}
if constants.DISABLE_EMAIL_VERIFICATION == "" && constants.DISABLE_BASIC_AUTHENTICATION == "false" {
if constants.SMTP_HOST == "" || constants.SENDER_EMAIL == "" || constants.SENDER_PASSWORD == "" {
constants.DISABLE_EMAIL_VERIFICATION = "true"
} else {
constants.DISABLE_EMAIL_VERIFICATION = "false"
}
}
rolesSplit := strings.Split(os.Getenv("ROLES"), ",")
roles := []string{}
if len(rolesSplit) == 0 {
roles = []string{"user"}
}
defaultRoleSplit := strings.Split(os.Getenv("DEFAULT_ROLES"), ",")
defaultRoles := []string{}
if len(defaultRoleSplit) == 0 {
defaultRoles = []string{"user"}
}
protectedRolesSplit := strings.Split(os.Getenv("PROTECTED_ROLES"), ",")
protectedRoles := []string{}
if len(protectedRolesSplit) > 0 {
for _, val := range protectedRolesSplit {
trimVal := strings.TrimSpace(val)
protectedRoles = append(protectedRoles, trimVal)
}
}
for _, val := range rolesSplit {
trimVal := strings.TrimSpace(val)
if trimVal != "" {
roles = append(roles, trimVal)
}
if utils.StringSliceContains(defaultRoleSplit, trimVal) {
defaultRoles = append(defaultRoles, trimVal)
}
}
if len(roles) > 0 && len(defaultRoles) == 0 && len(defaultRoleSplit) > 0 {
panic(`Invalid DEFAULT_ROLE environment variable. It can be one from give ROLES environment variable value`)
}
constants.ROLES = roles
constants.DEFAULT_ROLES = defaultRoles
constants.PROTECTED_ROLES = protectedRoles
if constants.JWT_ROLE_CLAIM == "" {
constants.JWT_ROLE_CLAIM = "role"
}
if os.Getenv("ORGANIZATION_NAME") != "" {
constants.ORGANIZATION_NAME = os.Getenv("ORGANIZATION_NAME")
}
if os.Getenv("ORGANIZATION_LOGO") != "" {
constants.ORGANIZATION_LOGO = os.Getenv("ORGANIZATION_LOGO")
}
}

278
server/env/env.go vendored Normal file
View File

@@ -0,0 +1,278 @@
package env
import (
"log"
"os"
"strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/joho/godotenv"
)
// build variables
var (
ARG_DB_URL *string
ARG_DB_TYPE *string
ARG_AUTHORIZER_URL *string
ARG_ENV_FILE *string
)
// InitEnv -> to initialize env and through error if required env are not present
func InitEnv() {
if constants.ENV_PATH == "" {
constants.ENV_PATH = `.env`
}
if ARG_ENV_FILE != nil && *ARG_ENV_FILE != "" {
constants.ENV_PATH = *ARG_ENV_FILE
}
err := godotenv.Load(constants.ENV_PATH)
if err != nil {
log.Printf("error loading %s file", constants.ENV_PATH)
}
if constants.ADMIN_SECRET == "" {
constants.ADMIN_SECRET = os.Getenv("ADMIN_SECRET")
if constants.ADMIN_SECRET == "" {
panic("root admin secret is required")
}
}
if constants.ENV == "" {
constants.ENV = os.Getenv("ENV")
if constants.ENV == "" {
constants.ENV = "production"
}
if constants.ENV == "production" {
constants.IS_PROD = true
os.Setenv("GIN_MODE", "release")
} else {
constants.IS_PROD = false
}
}
if constants.DATABASE_TYPE == "" {
constants.DATABASE_TYPE = os.Getenv("DATABASE_TYPE")
log.Println(constants.DATABASE_TYPE)
if ARG_DB_TYPE != nil && *ARG_DB_TYPE != "" {
constants.DATABASE_TYPE = *ARG_DB_TYPE
}
if constants.DATABASE_TYPE == "" {
panic("DATABASE_TYPE is required")
}
}
if constants.DATABASE_URL == "" {
constants.DATABASE_URL = os.Getenv("DATABASE_URL")
if ARG_DB_URL != nil && *ARG_DB_URL != "" {
constants.DATABASE_URL = *ARG_DB_URL
}
if constants.DATABASE_URL == "" {
panic("DATABASE_URL is required")
}
}
if constants.DATABASE_NAME == "" {
constants.DATABASE_NAME = os.Getenv("DATABASE_NAME")
if constants.DATABASE_NAME == "" {
constants.DATABASE_NAME = "authorizer"
}
}
if constants.SMTP_HOST == "" {
constants.SMTP_HOST = os.Getenv("SMTP_HOST")
}
if constants.SMTP_PORT == "" {
constants.SMTP_PORT = os.Getenv("SMTP_PORT")
}
if constants.SMTP_USERNAME == "" {
constants.SMTP_USERNAME = os.Getenv("SMTP_USERNAME")
}
if constants.SMTP_PASSWORD == "" {
constants.SMTP_PASSWORD = os.Getenv("SMTP_PASSWORD")
}
if constants.SENDER_EMAIL == "" {
constants.SENDER_EMAIL = os.Getenv("SENDER_EMAIL")
}
if constants.JWT_SECRET == "" {
constants.JWT_SECRET = os.Getenv("JWT_SECRET")
}
if constants.JWT_TYPE == "" {
constants.JWT_TYPE = os.Getenv("JWT_TYPE")
}
if constants.JWT_ROLE_CLAIM == "" {
constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM")
if constants.JWT_ROLE_CLAIM == "" {
constants.JWT_ROLE_CLAIM = "role"
}
}
if constants.AUTHORIZER_URL == "" {
constants.AUTHORIZER_URL = strings.TrimSuffix(os.Getenv("AUTHORIZER_URL"), "/")
if ARG_AUTHORIZER_URL != nil && *ARG_AUTHORIZER_URL != "" {
constants.AUTHORIZER_URL = *ARG_AUTHORIZER_URL
}
}
if constants.PORT == "" {
constants.PORT = os.Getenv("PORT")
if constants.PORT == "" {
constants.PORT = "8080"
}
}
if constants.REDIS_URL == "" {
constants.REDIS_URL = os.Getenv("REDIS_URL")
}
if constants.COOKIE_NAME == "" {
constants.COOKIE_NAME = os.Getenv("COOKIE_NAME")
}
if constants.GOOGLE_CLIENT_ID == "" {
constants.GOOGLE_CLIENT_ID = os.Getenv("GOOGLE_CLIENT_ID")
}
if constants.GOOGLE_CLIENT_SECRET == "" {
constants.GOOGLE_CLIENT_SECRET = os.Getenv("GOOGLE_CLIENT_SECRET")
}
if constants.GITHUB_CLIENT_ID == "" {
constants.GITHUB_CLIENT_ID = os.Getenv("GITHUB_CLIENT_ID")
}
if constants.GITHUB_CLIENT_SECRET == "" {
constants.GITHUB_CLIENT_SECRET = os.Getenv("GITHUB_CLIENT_SECRET")
}
if constants.FACEBOOK_CLIENT_ID == "" {
constants.FACEBOOK_CLIENT_ID = os.Getenv("FACEBOOK_CLIENT_ID")
}
if constants.FACEBOOK_CLIENT_SECRET == "" {
constants.FACEBOOK_CLIENT_SECRET = os.Getenv("FACEBOOK_CLIENT_SECRET")
}
if constants.RESET_PASSWORD_URL == "" {
constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/")
}
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION") == "true"
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION") == "true"
constants.DISABLE_MAGIC_LINK_LOGIN = os.Getenv("DISABLE_MAGIC_LINK_LOGIN") == "true"
constants.DISABLE_LOGIN_PAGE = os.Getenv("DISABLE_LOGIN_PAGE") == "true"
if constants.SMTP_HOST == "" || constants.SMTP_USERNAME == "" || constants.SMTP_PASSWORD == "" || constants.SENDER_EMAIL == "" {
constants.DISABLE_EMAIL_VERIFICATION = true
constants.DISABLE_MAGIC_LINK_LOGIN = true
}
allowedOriginsSplit := strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",")
allowedOrigins := []string{}
hasWildCard := false
for _, val := range allowedOriginsSplit {
trimVal := strings.TrimSpace(val)
if trimVal != "" {
if trimVal != "*" {
host, port := utils.GetHostParts(trimVal)
allowedOrigins = append(allowedOrigins, host+":"+port)
} else {
hasWildCard = true
allowedOrigins = append(allowedOrigins, trimVal)
break
}
}
}
if len(allowedOrigins) > 1 && hasWildCard {
allowedOrigins = []string{"*"}
}
if len(allowedOrigins) == 0 {
allowedOrigins = []string{"*"}
}
constants.ALLOWED_ORIGINS = allowedOrigins
if constants.JWT_TYPE == "" {
constants.JWT_TYPE = "HS256"
}
if constants.COOKIE_NAME == "" {
constants.COOKIE_NAME = "authorizer"
}
if constants.DISABLE_EMAIL_VERIFICATION {
constants.DISABLE_MAGIC_LINK_LOGIN = true
}
rolesEnv := strings.TrimSpace(os.Getenv("ROLES"))
rolesSplit := strings.Split(rolesEnv, ",")
roles := []string{}
if len(rolesEnv) == 0 {
roles = []string{"user"}
}
defaultRolesEnv := strings.TrimSpace(os.Getenv("DEFAULT_ROLES"))
defaultRoleSplit := strings.Split(defaultRolesEnv, ",")
defaultRoles := []string{}
if len(defaultRolesEnv) == 0 {
defaultRoles = []string{"user"}
}
protectedRolesEnv := strings.TrimSpace(os.Getenv("PROTECTED_ROLES"))
protectedRolesSplit := strings.Split(protectedRolesEnv, ",")
protectedRoles := []string{}
if len(protectedRolesEnv) > 0 {
for _, val := range protectedRolesSplit {
trimVal := strings.TrimSpace(val)
protectedRoles = append(protectedRoles, trimVal)
}
}
for _, val := range rolesSplit {
trimVal := strings.TrimSpace(val)
if trimVal != "" {
roles = append(roles, trimVal)
}
if utils.StringSliceContains(defaultRoleSplit, trimVal) {
defaultRoles = append(defaultRoles, trimVal)
}
}
if len(roles) > 0 && len(defaultRoles) == 0 && len(defaultRoleSplit) > 0 {
panic(`Invalid DEFAULT_ROLE environment variable. It can be one from give ROLES environment variable value`)
}
constants.ROLES = roles
constants.DEFAULT_ROLES = defaultRoles
constants.PROTECTED_ROLES = protectedRoles
if os.Getenv("ORGANIZATION_NAME") != "" {
constants.ORGANIZATION_NAME = os.Getenv("ORGANIZATION_NAME")
}
if os.Getenv("ORGANIZATION_LOGO") != "" {
constants.ORGANIZATION_LOGO = os.Getenv("ORGANIZATION_LOGO")
}
}

View File

@@ -3,30 +3,38 @@ module github.com/authorizerdev/authorizer/server
go 1.16 go 1.16
require ( require (
github.com/99designs/gqlgen v0.13.0 github.com/99designs/gqlgen v0.14.0
github.com/gin-contrib/location v0.0.2 // indirect github.com/arangodb/go-driver v1.2.1
github.com/coreos/go-oidc/v3 v3.1.0
github.com/gin-contrib/location v0.0.2
github.com/gin-gonic/gin v1.7.2 github.com/gin-gonic/gin v1.7.2
github.com/go-playground/validator/v10 v10.8.0 // indirect github.com/go-playground/validator/v10 v10.8.0 // indirect
github.com/go-redis/redis/v8 v8.11.0 github.com/go-redis/redis/v8 v8.11.0
github.com/golang-jwt/jwt v3.2.1+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/jackc/pgproto3/v2 v2.1.0 // indirect
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
github.com/json-iterator/go v1.1.11 // indirect github.com/json-iterator/go v1.1.11 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.7 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f
github.com/stretchr/testify v1.7.0
github.com/ugorji/go v1.2.6 // indirect github.com/ugorji/go v1.2.6 // indirect
github.com/vektah/gqlparser/v2 v2.1.0 github.com/vektah/gqlparser/v2 v2.2.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 go.mongodb.org/mongo-driver v1.8.1
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/mysql v1.1.1 gorm.io/driver/mysql v1.2.1
gorm.io/driver/postgres v1.1.0 gorm.io/driver/postgres v1.2.3
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.2.6
gorm.io/gorm v1.21.11 gorm.io/driver/sqlserver v1.2.1
gorm.io/gorm v1.22.4
) )

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,20 +2,11 @@
package model package model
type AdminUpdateUserInput struct {
ID string `json:"id"`
Email *string `json:"email"`
FirstName *string `json:"firstName"`
LastName *string `json:"lastName"`
Image *string `json:"image"`
Roles []*string `json:"roles"`
}
type AuthResponse struct { type AuthResponse struct {
Message string `json:"message"` Message string `json:"message"`
AccessToken *string `json:"accessToken"` AccessToken *string `json:"access_token"`
AccessTokenExpiresAt *int64 `json:"accessTokenExpiresAt"` ExpiresAt *int64 `json:"expires_at"`
User *User `json:"user"` User *User `json:"user"`
} }
type DeleteUserInput struct { type DeleteUserInput struct {
@@ -37,24 +28,30 @@ type LoginInput struct {
Roles []string `json:"roles"` Roles []string `json:"roles"`
} }
type MagicLinkLoginInput struct {
Email string `json:"email"`
Roles []string `json:"roles"`
}
type Meta struct { type Meta struct {
Version string `json:"version"` Version string `json:"version"`
IsGoogleLoginEnabled bool `json:"isGoogleLoginEnabled"` IsGoogleLoginEnabled bool `json:"is_google_login_enabled"`
IsFacebookLoginEnabled bool `json:"isFacebookLoginEnabled"` IsFacebookLoginEnabled bool `json:"is_facebook_login_enabled"`
IsTwitterLoginEnabled bool `json:"isTwitterLoginEnabled"` IsGithubLoginEnabled bool `json:"is_github_login_enabled"`
IsGithubLoginEnabled bool `json:"isGithubLoginEnabled"` IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
IsEmailVerificationEnabled bool `json:"isEmailVerificationEnabled"` IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
IsBasicAuthenticationEnabled bool `json:"isBasicAuthenticationEnabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
} }
type ResendVerifyEmailInput struct { type ResendVerifyEmailInput struct {
Email string `json:"email"` Email string `json:"email"`
Identifier string `json:"identifier"`
} }
type ResetPasswordInput struct { type ResetPasswordInput struct {
Token string `json:"token"` Token string `json:"token"`
Password string `json:"password"` Password string `json:"password"`
ConfirmPassword string `json:"confirmPassword"` ConfirmPassword string `json:"confirm_password"`
} }
type Response struct { type Response struct {
@@ -62,36 +59,67 @@ type Response struct {
} }
type SignUpInput struct { type SignUpInput struct {
FirstName *string `json:"firstName"`
LastName *string `json:"lastName"`
Email string `json:"email"` Email string `json:"email"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
Password string `json:"password"` Password string `json:"password"`
ConfirmPassword string `json:"confirmPassword"` ConfirmPassword string `json:"confirm_password"`
Image *string `json:"image"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
} }
type UpdateProfileInput struct { type UpdateProfileInput struct {
OldPassword *string `json:"oldPassword"` OldPassword *string `json:"old_password"`
NewPassword *string `json:"newPassword"` NewPassword *string `json:"new_password"`
ConfirmNewPassword *string `json:"confirmNewPassword"` ConfirmNewPassword *string `json:"confirm_new_password"`
FirstName *string `json:"firstName"`
LastName *string `json:"lastName"`
Image *string `json:"image"`
Email *string `json:"email"` Email *string `json:"email"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
}
type UpdateUserInput struct {
ID string `json:"id"`
Email *string `json:"email"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
Roles []*string `json:"roles"`
} }
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
Email string `json:"email"` Email string `json:"email"`
SignupMethod string `json:"signupMethod"` EmailVerified bool `json:"email_verified"`
FirstName *string `json:"firstName"` SignupMethods string `json:"signup_methods"`
LastName *string `json:"lastName"` GivenName *string `json:"given_name"`
EmailVerifiedAt *int64 `json:"emailVerifiedAt"` FamilyName *string `json:"family_name"`
Image *string `json:"image"` MiddleName *string `json:"middle_name"`
CreatedAt *int64 `json:"createdAt"` Nickname *string `json:"nickname"`
UpdatedAt *int64 `json:"updatedAt"` PreferredUsername *string `json:"preferred_username"`
Roles []string `json:"roles"` Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
PhoneNumberVerified *bool `json:"phone_number_verified"`
Picture *string `json:"picture"`
Roles []string `json:"roles"`
CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"`
} }
type VerificationRequest struct { type VerificationRequest struct {
@@ -100,8 +128,8 @@ type VerificationRequest struct {
Token *string `json:"token"` Token *string `json:"token"`
Email *string `json:"email"` Email *string `json:"email"`
Expires *int64 `json:"expires"` Expires *int64 `json:"expires"`
CreatedAt *int64 `json:"createdAt"` CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updatedAt"` UpdatedAt *int64 `json:"updated_at"`
} }
type VerifyEmailInput struct { type VerifyEmailInput struct {

View File

@@ -7,25 +7,33 @@ scalar Any
type Meta { type Meta {
version: String! version: String!
isGoogleLoginEnabled: Boolean! is_google_login_enabled: Boolean!
isFacebookLoginEnabled: Boolean! is_facebook_login_enabled: Boolean!
isTwitterLoginEnabled: Boolean! is_github_login_enabled: Boolean!
isGithubLoginEnabled: Boolean! is_email_verification_enabled: Boolean!
isEmailVerificationEnabled: Boolean! is_basic_authentication_enabled: Boolean!
isBasicAuthenticationEnabled: Boolean! is_magic_link_login_enabled: Boolean!
} }
type User { type User {
id: ID! id: ID!
email: String! email: String!
signupMethod: String! email_verified: Boolean!
firstName: String signup_methods: String!
lastName: String given_name: String
emailVerifiedAt: Int64 family_name: String
image: String middle_name: String
createdAt: Int64 nickname: String
updatedAt: Int64 # defaults to email
preferred_username: String
gender: String
birthdate: String
phone_number: String
phone_number_verified: Boolean
picture: String
roles: [String!]! roles: [String!]!
created_at: Int64
updated_at: Int64
} }
type VerificationRequest { type VerificationRequest {
@@ -34,8 +42,8 @@ type VerificationRequest {
token: String token: String
email: String email: String
expires: Int64 expires: Int64
createdAt: Int64 created_at: Int64
updatedAt: Int64 updated_at: Int64
} }
type Error { type Error {
@@ -45,8 +53,8 @@ type Error {
type AuthResponse { type AuthResponse {
message: String! message: String!
accessToken: String access_token: String
accessTokenExpiresAt: Int64 expires_at: Int64
user: User user: User
} }
@@ -55,12 +63,17 @@ type Response {
} }
input SignUpInput { input SignUpInput {
firstName: String
lastName: String
email: String! email: String!
given_name: String
family_name: String
middle_name: String
nickname: String
gender: String
birthdate: String
phone_number: String
picture: String
password: String! password: String!
confirmPassword: String! confirm_password: String!
image: String
roles: [String!] roles: [String!]
} }
@@ -76,25 +89,35 @@ input VerifyEmailInput {
input ResendVerifyEmailInput { input ResendVerifyEmailInput {
email: String! email: String!
identifier: String!
} }
input UpdateProfileInput { input UpdateProfileInput {
oldPassword: String old_password: String
newPassword: String new_password: String
confirmNewPassword: String confirm_new_password: String
firstName: String
lastName: String
image: String
email: String email: String
# roles: [String] given_name: String
family_name: String
middle_name: String
nickname: String
gender: String
birthdate: String
phone_number: String
picture: String
} }
input AdminUpdateUserInput { input UpdateUserInput {
id: ID! id: ID!
email: String email: String
firstName: String given_name: String
lastName: String family_name: String
image: String middle_name: String
nickname: String
gender: String
birthdate: String
phone_number: String
picture: String
roles: [String] roles: [String]
} }
@@ -105,30 +128,38 @@ input ForgotPasswordInput {
input ResetPasswordInput { input ResetPasswordInput {
token: String! token: String!
password: String! password: String!
confirmPassword: String! confirm_password: String!
} }
input DeleteUserInput { input DeleteUserInput {
email: String! email: String!
} }
input MagicLinkLoginInput {
email: String!
roles: [String!]
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
magic_link_login(params: MagicLinkLoginInput!): Response!
logout: Response! logout: Response!
updateProfile(params: UpdateProfileInput!): Response! update_profile(params: UpdateProfileInput!): Response!
adminUpdateUser(params: AdminUpdateUserInput!): User! verify_email(params: VerifyEmailInput!): AuthResponse!
verifyEmail(params: VerifyEmailInput!): AuthResponse! resend_verify_email(params: ResendVerifyEmailInput!): Response!
resendVerifyEmail(params: ResendVerifyEmailInput!): Response! forgot_password(params: ForgotPasswordInput!): Response!
forgotPassword(params: ForgotPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response!
resetPassword(params: ResetPasswordInput!): Response! # admin only apis
deleteUser(params: DeleteUserInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User!
} }
type Query { type Query {
meta: Meta! meta: Meta!
users: [User!]! session(roles: [String!]): AuthResponse
token(roles: [String!]): AuthResponse
profile: User! profile: User!
verificationRequests: [VerificationRequest!]! # admin only apis
_users: [User!]!
_verification_requests: [VerificationRequest!]!
} }

View File

@@ -19,6 +19,10 @@ func (r *mutationResolver) Login(ctx context.Context, params model.LoginInput) (
return resolvers.Login(ctx, params) return resolvers.Login(ctx, params)
} }
func (r *mutationResolver) MagicLinkLogin(ctx context.Context, params model.MagicLinkLoginInput) (*model.Response, error) {
return resolvers.MagicLinkLogin(ctx, params)
}
func (r *mutationResolver) Logout(ctx context.Context) (*model.Response, error) { func (r *mutationResolver) Logout(ctx context.Context) (*model.Response, error) {
return resolvers.Logout(ctx) return resolvers.Logout(ctx)
} }
@@ -27,10 +31,6 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, params model.Updat
return resolvers.UpdateProfile(ctx, params) return resolvers.UpdateProfile(ctx, params)
} }
func (r *mutationResolver) AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*model.User, error) {
return resolvers.AdminUpdateUser(ctx, params)
}
func (r *mutationResolver) VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.AuthResponse, error) { func (r *mutationResolver) VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.AuthResponse, error) {
return resolvers.VerifyEmail(ctx, params) return resolvers.VerifyEmail(ctx, params)
} }
@@ -51,22 +51,26 @@ func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUs
return resolvers.DeleteUser(ctx, params) return resolvers.DeleteUser(ctx, params)
} }
func (r *mutationResolver) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) {
return resolvers.UpdateUser(ctx, params)
}
func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) {
return resolvers.Meta(ctx) return resolvers.Meta(ctx)
} }
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) { func (r *queryResolver) Session(ctx context.Context, roles []string) (*model.AuthResponse, error) {
return resolvers.Users(ctx) return resolvers.Session(ctx, roles)
}
func (r *queryResolver) Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
return resolvers.Token(ctx, roles)
} }
func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) { func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) {
return resolvers.Profile(ctx) return resolvers.Profile(ctx)
} }
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
return resolvers.Users(ctx)
}
func (r *queryResolver) VerificationRequests(ctx context.Context) ([]*model.VerificationRequest, error) { func (r *queryResolver) VerificationRequests(ctx context.Context) ([]*model.VerificationRequest, error) {
return resolvers.VerificationRequests(ctx) return resolvers.VerificationRequests(ctx)
} }
@@ -77,5 +81,7 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol
// Query returns generated.QueryResolver implementation. // Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver } type (
type queryResolver struct{ *Resolver } mutationResolver struct{ *Resolver }
queryResolver struct{ *Resolver }
)

View File

@@ -49,7 +49,7 @@ func AppHandler() gin.HandlerFunc {
stateObj.RedirectURL = strings.TrimSuffix(stateObj.RedirectURL, "/") stateObj.RedirectURL = strings.TrimSuffix(stateObj.RedirectURL, "/")
// validate redirect url with allowed origins // validate redirect url with allowed origins
if !utils.IsValidRedirectURL(stateObj.RedirectURL) { if !utils.IsValidOrigin(stateObj.RedirectURL) {
c.JSON(400, gin.H{"error": "invalid redirect url"}) c.JSON(400, gin.H{"error": "invalid redirect url"})
return return
} }

View File

@@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@@ -15,37 +16,35 @@ import (
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/session"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
func processGoogleUserInfo(code string) (db.User, error) { func processGoogleUserInfo(code string) (db.User, error) {
user := db.User{} user := db.User{}
token, err := oauth.OAuthProvider.GoogleConfig.Exchange(oauth2.NoContext, code) ctx := context.Background()
oauth2Token, err := oauth.OAuthProviders.GoogleConfig.Exchange(ctx, code)
if err != nil { if err != nil {
return user, fmt.Errorf("invalid google exchange code: %s", err.Error()) return user, fmt.Errorf("invalid google exchange code: %s", err.Error())
} }
client := oauth.OAuthProvider.GoogleConfig.Client(oauth2.NoContext, token)
response, err := client.Get(constants.GoogleUserInfoURL) verifier := oauth.OIDCProviders.GoogleOIDC.Verifier(&oidc.Config{ClientID: oauth.OAuthProviders.GoogleConfig.ClientID})
if err != nil {
return user, err // Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return user, fmt.Errorf("unable to extract id_token")
} }
defer response.Body.Close() // Parse and verify ID Token payload.
body, err := ioutil.ReadAll(response.Body) idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil { if err != nil {
return user, fmt.Errorf("failed to read google response body: %s", err.Error()) return user, fmt.Errorf("unable to verify id_token: %s", err.Error())
} }
userRawData := make(map[string]string) if err := idToken.Claims(&user); err != nil {
json.Unmarshal(body, &userRawData) return user, fmt.Errorf("unable to extract claims")
user = db.User{
FirstName: userRawData["given_name"],
LastName: userRawData["family_name"],
Image: userRawData["picture"],
Email: userRawData["email"],
EmailVerifiedAt: time.Now().Unix(),
} }
return user, nil return user, nil
@@ -53,7 +52,7 @@ func processGoogleUserInfo(code string) (db.User, error) {
func processGithubUserInfo(code string) (db.User, error) { func processGithubUserInfo(code string) (db.User, error) {
user := db.User{} user := db.User{}
token, err := oauth.OAuthProvider.GithubConfig.Exchange(oauth2.NoContext, code) token, err := oauth.OAuthProviders.GithubConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
return user, fmt.Errorf("invalid github exchange code: %s", err.Error()) return user, fmt.Errorf("invalid github exchange code: %s", err.Error())
} }
@@ -89,12 +88,14 @@ func processGithubUserInfo(code string) (db.User, error) {
if len(name) > 1 && strings.TrimSpace(name[1]) != "" { if len(name) > 1 && strings.TrimSpace(name[1]) != "" {
lastName = name[0] lastName = name[0]
} }
picture := userRawData["avatar_url"]
user = db.User{ user = db.User{
FirstName: firstName, GivenName: &firstName,
LastName: lastName, FamilyName: &lastName,
Image: userRawData["avatar_url"], Picture: &picture,
Email: userRawData["email"], Email: userRawData["email"],
EmailVerifiedAt: time.Now().Unix(),
} }
return user, nil return user, nil
@@ -102,7 +103,7 @@ func processGithubUserInfo(code string) (db.User, error) {
func processFacebookUserInfo(code string) (db.User, error) { func processFacebookUserInfo(code string) (db.User, error) {
user := db.User{} user := db.User{}
token, err := oauth.OAuthProvider.FacebookConfig.Exchange(oauth2.NoContext, code) token, err := oauth.OAuthProviders.FacebookConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
return user, fmt.Errorf("invalid facebook exchange code: %s", err.Error()) return user, fmt.Errorf("invalid facebook exchange code: %s", err.Error())
} }
@@ -114,7 +115,7 @@ func processFacebookUserInfo(code string) (db.User, error) {
response, err := client.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
log.Println("err:", err) log.Println("error processing facebook user info:", err)
return user, err return user, err
} }
@@ -131,12 +132,15 @@ func processFacebookUserInfo(code string) (db.User, error) {
picObject := userRawData["picture"].(map[string]interface{})["data"] picObject := userRawData["picture"].(map[string]interface{})["data"]
picDataObject := picObject.(map[string]interface{}) picDataObject := picObject.(map[string]interface{})
firstName := fmt.Sprintf("%v", userRawData["first_name"])
lastName := fmt.Sprintf("%v", userRawData["last_name"])
picture := fmt.Sprintf("%v", picDataObject["url"])
user = db.User{ user = db.User{
FirstName: fmt.Sprintf("%v", userRawData["first_name"]), GivenName: &firstName,
LastName: fmt.Sprintf("%v", userRawData["last_name"]), FamilyName: &lastName,
Image: fmt.Sprintf("%v", picDataObject["url"]), Picture: &picture,
Email: email, Email: email,
EmailVerifiedAt: time.Now().Unix(),
} }
return user, nil return user, nil
@@ -187,7 +191,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
if err != nil { if err != nil {
// user not registered, register user and generate session token // user not registered, register user and generate session token
user.SignupMethod = provider user.SignupMethods = provider
// make sure inputRoles don't include protected roles // make sure inputRoles don't include protected roles
hasProtectedRole := false hasProtectedRole := false
for _, ir := range inputRoles { for _, ir := range inputRoles {
@@ -202,15 +206,18 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} }
user.Roles = strings.Join(inputRoles, ",") user.Roles = strings.Join(inputRoles, ",")
now := time.Now().Unix()
user.EmailVerifiedAt = &now
user, _ = db.Mgr.AddUser(user)
} else { } else {
// user exists in db, check if method was google // user exists in db, check if method was google
// if not append google to existing signup method and save it // if not append google to existing signup method and save it
signupMethod := existingUser.SignupMethod signupMethod := existingUser.SignupMethods
if !strings.Contains(signupMethod, provider) { if !strings.Contains(signupMethod, provider) {
signupMethod = signupMethod + "," + enum.Github.String() signupMethod = signupMethod + "," + provider
} }
user.SignupMethod = signupMethod user.SignupMethods = signupMethod
user.Password = existingUser.Password user.Password = existingUser.Password
// There multiple scenarios with roles here in social login // There multiple scenarios with roles here in social login
@@ -245,9 +252,11 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} else { } else {
user.Roles = existingUser.Roles user.Roles = existingUser.Roles
} }
user.Key = existingUser.Key
user.ID = existingUser.ID
user, err = db.Mgr.UpdateUser(user)
} }
user, _ = db.Mgr.SaveUser(user)
user, _ = db.Mgr.GetUserByEmail(user.Email) user, _ = db.Mgr.GetUserByEmail(user.Email)
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, inputRoles) refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, inputRoles)
@@ -255,15 +264,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, inputRoles) accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, inputRoles)
utils.SetCookie(c, accessToken) utils.SetCookie(c, accessToken)
session.SetToken(userIdStr, accessToken, refreshToken) session.SetToken(userIdStr, accessToken, refreshToken)
go func() { utils.CreateSession(user.ID, c)
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(c.Request),
IP: utils.GetIP(c.Request),
}
db.Mgr.SaveSession(sessionData)
}()
c.Redirect(http.StatusTemporaryRedirect, redirectURL) c.Redirect(http.StatusTemporaryRedirect, redirectURL)
} }

View File

@@ -48,28 +48,46 @@ func OAuthLoginHandler() gin.HandlerFunc {
oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")
isProviderConfigured := true
switch provider { switch provider {
case enum.Google.String(): case enum.Google.String():
if oauth.OAuthProviders.GoogleConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Google.String()) session.SetSocailLoginState(oauthStateString, enum.Google.String())
// during the init of OAuthProvider authorizer url might be empty // during the init of OAuthProvider authorizer url might be empty
oauth.OAuthProvider.GoogleConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/google" oauth.OAuthProviders.GoogleConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/google"
url := oauth.OAuthProvider.GoogleConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case enum.Github.String(): case enum.Github.String():
if oauth.OAuthProviders.GithubConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Github.String()) session.SetSocailLoginState(oauthStateString, enum.Github.String())
oauth.OAuthProvider.GithubConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/github" oauth.OAuthProviders.GithubConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/github"
url := oauth.OAuthProvider.GithubConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case enum.Facebook.String(): case enum.Facebook.String():
if oauth.OAuthProviders.FacebookConfig == nil {
isProviderConfigured = false
break
}
session.SetSocailLoginState(oauthStateString, enum.Facebook.String()) session.SetSocailLoginState(oauthStateString, enum.Facebook.String())
oauth.OAuthProvider.FacebookConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/facebook" oauth.OAuthProviders.FacebookConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/facebook"
url := oauth.OAuthProvider.FacebookConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
default: default:
c.JSON(422, gin.H{ c.JSON(422, gin.H{
"message": "Invalid oauth provider", "message": "Invalid oauth provider",
}) })
} }
if !isProviderConfigured {
c.JSON(422, gin.H{
"message": provider + " not configured",
})
}
} }
} }

View File

@@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -24,7 +23,7 @@ func VerifyEmailHandler() gin.HandlerFunc {
return return
} }
_, err := db.Mgr.GetVerificationByToken(token) verificationRequest, err := db.Mgr.GetVerificationByToken(token)
if err != nil { if err != nil {
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
@@ -46,27 +45,22 @@ func VerifyEmailHandler() gin.HandlerFunc {
} }
// update email_verified_at in users table // update email_verified_at in users table
db.Mgr.UpdateVerificationTime(time.Now().Unix(), user.ID) if user.EmailVerifiedAt == nil {
now := time.Now().Unix()
user.EmailVerifiedAt = &now
db.Mgr.UpdateUser(user)
}
// delete from verification table // delete from verification table
db.Mgr.DeleteToken(claim.Email) db.Mgr.DeleteVerificationRequest(verificationRequest)
userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles) refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles)
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, roles) accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, accessToken, refreshToken) session.SetToken(user.ID, accessToken, refreshToken)
go func() { utils.CreateSession(user.ID, c)
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(c.Request),
IP: utils.GetIP(c.Request),
}
db.Mgr.SaveSession(sessionData)
}()
utils.SetCookie(c, accessToken) utils.SetCookie(c, accessToken)
c.Redirect(http.StatusTemporaryRedirect, claim.Host) c.Redirect(http.StatusTemporaryRedirect, claim.RedirectURL)
} }
} }

View File

@@ -1,78 +1,48 @@
package main package main
import ( import (
"context" "flag"
"log"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/env"
"github.com/authorizerdev/authorizer/server/handlers" "github.com/authorizerdev/authorizer/server/handlers"
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/router"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/session"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin"
) )
func GinContextToContextMiddleware() gin.HandlerFunc { var VERSION string
return func(c *gin.Context) {
if constants.AUTHORIZER_URL == "" {
url := location.Get(c)
constants.AUTHORIZER_URL = url.Scheme + "://" + c.Request.Host
log.Println("=> setting url:", constants.AUTHORIZER_URL)
}
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
// TODO use allowed origins for cors origin
// TODO throw error if url is not allowed
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func main() { func main() {
InitEnv() env.ARG_DB_URL = flag.String("database_url", "", "Database connection string")
env.ARG_DB_TYPE = flag.String("database_type", "", "Database type, possible values are postgres,mysql,sqlite")
env.ARG_AUTHORIZER_URL = flag.String("authorizer_url", "", "URL for authorizer instance, eg: https://xyz.herokuapp.com")
env.ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
flag.Parse()
constants.VERSION = VERSION
env.InitEnv()
db.InitDB() db.InitDB()
session.InitSession() session.InitSession()
oauth.InitOAuth() oauth.InitOAuth()
utils.InitServer() utils.InitServer()
r := gin.Default() router := router.InitRouter()
r.Use(location.Default())
r.Use(GinContextToContextMiddleware())
r.Use(CORSMiddleware())
r.GET("/", handlers.PlaygroundHandler()) // login page app related routes.
r.POST("/graphql", handlers.GraphqlHandler()) // if we put them in router file then tests would fail as templates or build path will be different
r.GET("/verify_email", handlers.VerifyEmailHandler()) if !constants.DISABLE_LOGIN_PAGE {
r.GET("/oauth_login/:oauth_provider", handlers.OAuthLoginHandler()) router.LoadHTMLGlob("templates/*")
r.GET("/oauth_callback/:oauth_provider", handlers.OAuthCallbackHandler()) app := router.Group("/app")
{
// login wall app related routes app.Static("/build", "app/build")
app.GET("/", handlers.AppHandler())
r.LoadHTMLGlob("templates/*") app.GET("/reset-password", handlers.AppHandler())
app := r.Group("/app") }
{
app.Static("/build", "app/build")
app.GET("/", handlers.AppHandler())
app.GET("/reset-password", handlers.AppHandler())
} }
r.Run() router.Run(":" + constants.PORT)
} }

View File

@@ -0,0 +1,23 @@
package middlewares
import (
"context"
"log"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin"
)
func GinContextToContextMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if constants.AUTHORIZER_URL == "" {
url := location.Get(c)
constants.AUTHORIZER_URL = url.Scheme + "://" + c.Request.Host
log.Println("authorizer url:", constants.AUTHORIZER_URL)
}
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}

View File

@@ -0,0 +1,29 @@
package middlewares
import (
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin"
)
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
constants.APP_URL = origin
if utils.IsValidOrigin(origin) {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
}
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

View File

@@ -1,33 +1,49 @@
package oauth package oauth
import ( import (
"context"
"log"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
facebookOAuth2 "golang.org/x/oauth2/facebook" facebookOAuth2 "golang.org/x/oauth2/facebook"
githubOAuth2 "golang.org/x/oauth2/github" githubOAuth2 "golang.org/x/oauth2/github"
googleOAuth2 "golang.org/x/oauth2/google"
) )
type OAuthProviders struct { type OAuthProvider struct {
GoogleConfig *oauth2.Config GoogleConfig *oauth2.Config
GithubConfig *oauth2.Config GithubConfig *oauth2.Config
FacebookConfig *oauth2.Config FacebookConfig *oauth2.Config
} }
var OAuthProvider OAuthProviders type OIDCProvider struct {
GoogleOIDC *oidc.Provider
}
var (
OAuthProviders OAuthProvider
OIDCProviders OIDCProvider
)
func InitOAuth() { func InitOAuth() {
ctx := context.Background()
if constants.GOOGLE_CLIENT_ID != "" && constants.GOOGLE_CLIENT_SECRET != "" { if constants.GOOGLE_CLIENT_ID != "" && constants.GOOGLE_CLIENT_SECRET != "" {
OAuthProvider.GoogleConfig = &oauth2.Config{ p, err := oidc.NewProvider(ctx, "https://accounts.google.com")
if err != nil {
log.Fatalln("error creating oidc provider for google:", err)
}
OIDCProviders.GoogleOIDC = p
OAuthProviders.GoogleConfig = &oauth2.Config{
ClientID: constants.GOOGLE_CLIENT_ID, ClientID: constants.GOOGLE_CLIENT_ID,
ClientSecret: constants.GOOGLE_CLIENT_SECRET, ClientSecret: constants.GOOGLE_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/google", RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/google",
Endpoint: googleOAuth2.Endpoint, Endpoint: OIDCProviders.GoogleOIDC.Endpoint(),
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}, Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
} }
} }
if constants.GITHUB_CLIENT_ID != "" && constants.GITHUB_CLIENT_SECRET != "" { if constants.GITHUB_CLIENT_ID != "" && constants.GITHUB_CLIENT_SECRET != "" {
OAuthProvider.GithubConfig = &oauth2.Config{ OAuthProviders.GithubConfig = &oauth2.Config{
ClientID: constants.GITHUB_CLIENT_ID, ClientID: constants.GITHUB_CLIENT_ID,
ClientSecret: constants.GITHUB_CLIENT_SECRET, ClientSecret: constants.GITHUB_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/github", RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/github",
@@ -35,7 +51,7 @@ func InitOAuth() {
} }
} }
if constants.FACEBOOK_CLIENT_ID != "" && constants.FACEBOOK_CLIENT_SECRET != "" { if constants.FACEBOOK_CLIENT_ID != "" && constants.FACEBOOK_CLIENT_SECRET != "" {
OAuthProvider.FacebookConfig = &oauth2.Config{ OAuthProviders.FacebookConfig = &oauth2.Config{
ClientID: constants.FACEBOOK_CLIENT_ID, ClientID: constants.FACEBOOK_CLIENT_ID,
ClientSecret: constants.FACEBOOK_CLIENT_SECRET, ClientSecret: constants.FACEBOOK_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/facebook", RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/facebook",

View File

@@ -29,9 +29,9 @@ func DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Respo
session.DeleteUserSession(fmt.Sprintf("%x", user.ID)) session.DeleteUserSession(fmt.Sprintf("%x", user.ID))
err = db.Mgr.DeleteUser(params.Email) err = db.Mgr.DeleteUser(user)
if err != nil { if err != nil {
log.Println("Err:", err) log.Println("error deleting user:", err)
return res, err return res, err
} }

View File

@@ -20,7 +20,7 @@ func ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*mod
if err != nil { if err != nil {
return res, err return res, err
} }
if constants.DISABLE_BASIC_AUTHENTICATION == "true" { if constants.DISABLE_BASIC_AUTHENTICATION {
return res, fmt.Errorf(`basic authentication is disabled for this instance`) return res, fmt.Errorf(`basic authentication is disabled for this instance`)
} }
host := gc.Request.Host host := gc.Request.Host
@@ -37,7 +37,7 @@ func ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*mod
token, err := utils.CreateVerificationToken(params.Email, enum.ForgotPassword.String()) token, err := utils.CreateVerificationToken(params.Email, enum.ForgotPassword.String())
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,

View File

@@ -22,7 +22,7 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
return res, err return res, err
} }
if constants.DISABLE_BASIC_AUTHENTICATION == "true" { if constants.DISABLE_BASIC_AUTHENTICATION {
return res, fmt.Errorf(`basic authentication is disabled for this instance`) return res, fmt.Errorf(`basic authentication is disabled for this instance`)
} }
@@ -32,18 +32,18 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
return res, fmt.Errorf(`user with this email not found`) return res, fmt.Errorf(`user with this email not found`)
} }
if !strings.Contains(user.SignupMethod, enum.BasicAuth.String()) { if !strings.Contains(user.SignupMethods, enum.BasicAuth.String()) {
return res, fmt.Errorf(`user has not signed up email & password`) return res, fmt.Errorf(`user has not signed up email & password`)
} }
if user.EmailVerifiedAt <= 0 { if user.EmailVerifiedAt == nil {
return res, fmt.Errorf(`email not verified`) return res, fmt.Errorf(`email not verified`)
} }
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(params.Password)) err = bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(params.Password))
if err != nil { if err != nil {
log.Println("Compare password error:", err) log.Println("compare password error:", err)
return res, fmt.Errorf(`invalid password`) return res, fmt.Errorf(`invalid password`)
} }
roles := constants.DEFAULT_ROLES roles := constants.DEFAULT_ROLES
@@ -55,38 +55,18 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
roles = params.Roles roles = params.Roles
} }
userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles) refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles)
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles) accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, accessToken, refreshToken) session.SetToken(user.ID, accessToken, refreshToken)
go func() { utils.CreateSession(user.ID, gc)
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
}
db.Mgr.SaveSession(sessionData)
}()
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Logged in successfully`, Message: `Logged in successfully`,
AccessToken: &accessToken, AccessToken: &accessToken,
AccessTokenExpiresAt: &expiresAt, ExpiresAt: &expiresAt,
User: &model.User{ User: utils.GetResponseUserData(user),
ID: userIdStr,
Email: user.Email,
Image: &user.Image,
FirstName: &user.FirstName,
LastName: &user.LastName,
SignupMethod: user.SignupMethod,
EmailVerifiedAt: &user.EmailVerifiedAt,
Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
},
} }
utils.SetCookie(gc, accessToken) utils.SetCookie(gc, accessToken)

View File

@@ -27,7 +27,7 @@ func Logout(ctx context.Context) (*model.Response, error) {
} }
userId := fmt.Sprintf("%v", claim["id"]) userId := fmt.Sprintf("%v", claim["id"])
session.DeleteToken(userId, token) session.DeleteVerificationRequest(userId, token)
res = &model.Response{ res = &model.Response{
Message: "Logged out successfully", Message: "Logged out successfully",
} }

View File

@@ -0,0 +1,126 @@
package resolvers
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils"
)
func MagicLinkLogin(ctx context.Context, params model.MagicLinkLoginInput) (*model.Response, error) {
var res *model.Response
if constants.DISABLE_MAGIC_LINK_LOGIN {
return res, fmt.Errorf(`magic link login is disabled for this instance`)
}
params.Email = strings.ToLower(params.Email)
if !utils.IsValidEmail(params.Email) {
return res, fmt.Errorf(`invalid email address`)
}
inputRoles := []string{}
user := db.User{
Email: params.Email,
}
// find user with email
existingUser, err := db.Mgr.GetUserByEmail(params.Email)
if err != nil {
user.SignupMethods = enum.MagicLinkLogin.String()
// define roles for new user
if len(params.Roles) > 0 {
// check if roles exists
if !utils.IsValidRoles(constants.ROLES, params.Roles) {
return res, fmt.Errorf(`invalid roles`)
} else {
inputRoles = params.Roles
}
} else {
inputRoles = constants.DEFAULT_ROLES
}
user.Roles = strings.Join(inputRoles, ",")
user, _ = db.Mgr.AddUser(user)
} else {
user = existingUser
// There multiple scenarios with roles here in magic link login
// 1. user has access to protected roles + roles and trying to login
// 2. user has not signed up for one of the available role but trying to signup.
// Need to modify roles in this case
// find the unassigned roles
existingRoles := strings.Split(existingUser.Roles, ",")
unasignedRoles := []string{}
for _, ir := range inputRoles {
if !utils.StringSliceContains(existingRoles, ir) {
unasignedRoles = append(unasignedRoles, ir)
}
}
if len(unasignedRoles) > 0 {
// check if it contains protected unassigned role
hasProtectedRole := false
for _, ur := range unasignedRoles {
if utils.StringSliceContains(constants.PROTECTED_ROLES, ur) {
hasProtectedRole = true
}
}
if hasProtectedRole {
return res, fmt.Errorf(`invalid roles`)
} else {
user.Roles = existingUser.Roles + "," + strings.Join(unasignedRoles, ",")
}
} else {
user.Roles = existingUser.Roles
}
signupMethod := existingUser.SignupMethods
if !strings.Contains(signupMethod, enum.MagicLinkLogin.String()) {
signupMethod = signupMethod + "," + enum.MagicLinkLogin.String()
}
user.SignupMethods = signupMethod
user, _ = db.Mgr.UpdateUser(user)
if err != nil {
log.Println("error updating user:", err)
}
}
if !constants.DISABLE_EMAIL_VERIFICATION {
// insert verification request
verificationType := enum.MagicLinkLogin.String()
token, err := utils.CreateVerificationToken(params.Email, verificationType)
if err != nil {
log.Println(`error generating token`, err)
}
db.Mgr.AddVerification(db.VerificationRequest{
Token: token,
Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email,
})
// exec it as go routin so that we can reduce the api latency
go func() {
utils.SendVerificationMail(params.Email, token)
}()
}
res = &model.Response{
Message: `Magic Link has been sent to your email. Please check your inbox!`,
}
return res, nil
}

View File

@@ -3,7 +3,6 @@ package resolvers
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@@ -41,20 +40,7 @@ func Profile(ctx context.Context) (*model.User, error) {
return res, err return res, err
} }
userIdStr := fmt.Sprintf("%v", user.ID) res = utils.GetResponseUserData(user)
res = &model.User{
ID: userIdStr,
Email: user.Email,
Image: &user.Image,
FirstName: &user.FirstName,
LastName: &user.LastName,
SignupMethod: user.SignupMethod,
EmailVerifiedAt: &user.EmailVerifiedAt,
Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
}
return res, nil return res, nil
} }

View File

@@ -20,18 +20,28 @@ func ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput)
return res, fmt.Errorf("invalid email") return res, fmt.Errorf("invalid email")
} }
verificationRequest, err := db.Mgr.GetVerificationByEmail(params.Email) if !utils.IsValidVerificationIdentifier(params.Identifier) {
return res, fmt.Errorf("invalid identifier")
}
verificationRequest, err := db.Mgr.GetVerificationByEmail(params.Email, params.Identifier)
if err != nil { if err != nil {
return res, fmt.Errorf(`verification request not found`) return res, fmt.Errorf(`verification request not found`)
} }
token, err := utils.CreateVerificationToken(params.Email, verificationRequest.Identifier) // delete current verification and create new one
err = db.Mgr.DeleteVerificationRequest(verificationRequest)
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println("error deleting verification request:", err)
}
token, err := utils.CreateVerificationToken(params.Email, params.Identifier)
if err != nil {
log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,
Identifier: verificationRequest.Identifier, Identifier: params.Identifier,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
}) })

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
@@ -14,11 +15,11 @@ import (
func ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) { func ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) {
var res *model.Response var res *model.Response
if constants.DISABLE_BASIC_AUTHENTICATION == "true" { if constants.DISABLE_BASIC_AUTHENTICATION {
return res, fmt.Errorf(`basic authentication is disabled for this instance`) return res, fmt.Errorf(`basic authentication is disabled for this instance`)
} }
_, err := db.Mgr.GetVerificationByToken(params.Token) verificationRequest, err := db.Mgr.GetVerificationByToken(params.Token)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token`) return res, fmt.Errorf(`invalid token`)
} }
@@ -39,16 +40,22 @@ func ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model
} }
password, _ := utils.HashPassword(params.Password) password, _ := utils.HashPassword(params.Password)
user.Password = password user.Password = &password
signupMethod := user.SignupMethod signupMethod := user.SignupMethods
if !strings.Contains(signupMethod, enum.BasicAuth.String()) { if !strings.Contains(signupMethod, enum.BasicAuth.String()) {
signupMethod = signupMethod + "," + enum.BasicAuth.String() signupMethod = signupMethod + "," + enum.BasicAuth.String()
} }
user.SignupMethod = signupMethod user.SignupMethods = signupMethod
// helpful if user has not signed up with basic auth
if user.EmailVerifiedAt == nil {
now := time.Now().Unix()
user.EmailVerifiedAt = &now
}
// delete from verification table // delete from verification table
db.Mgr.DeleteToken(claim.Email) db.Mgr.DeleteVerificationRequest(verificationRequest)
db.Mgr.UpdateUser(user) db.Mgr.UpdateUser(user)
res = &model.Response{ res = &model.Response{

View File

@@ -3,7 +3,6 @@ package resolvers
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@@ -14,7 +13,7 @@ import (
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) { func Session(ctx context.Context, roles []string) (*model.AuthResponse, error) {
var res *model.AuthResponse var res *model.AuthResponse
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
@@ -64,35 +63,18 @@ func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
// if access token has expired and refresh/session token is valid // if access token has expired and refresh/session token is valid
// generate new accessToken // generate new accessToken
currentRefreshToken := session.GetToken(userIdStr, token) currentRefreshToken := session.GetToken(userIdStr, token)
session.DeleteToken(userIdStr, token) session.DeleteVerificationRequest(userIdStr, token)
token, expiresAt, _ = utils.CreateAuthToken(user, enum.AccessToken, claimRoles) token, expiresAt, _ = utils.CreateAuthToken(user, enum.AccessToken, claimRoles)
session.SetToken(userIdStr, token, currentRefreshToken) session.SetToken(userIdStr, token, currentRefreshToken)
go func() { utils.CreateSession(user.ID, gc)
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
}
db.Mgr.SaveSession(sessionData)
}()
} }
utils.SetCookie(gc, token) utils.SetCookie(gc, token)
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Token verified`, Message: `Token verified`,
AccessToken: &token, AccessToken: &token,
AccessTokenExpiresAt: &expiresAt, ExpiresAt: &expiresAt,
User: &model.User{ User: utils.GetResponseUserData(user),
ID: userIdStr,
Email: user.Email,
Image: &user.Image,
FirstName: &user.FirstName,
LastName: &user.LastName,
Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
},
} }
return res, nil return res, nil
} }

View File

@@ -22,11 +22,11 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
return res, err return res, err
} }
if constants.DISABLE_BASIC_AUTHENTICATION == "true" { if constants.DISABLE_BASIC_AUTHENTICATION {
return res, fmt.Errorf(`basic authentication is disabled for this instance`) return res, fmt.Errorf(`basic authentication is disabled for this instance`)
} }
if params.ConfirmPassword != params.Password { if params.ConfirmPassword != params.Password {
return res, fmt.Errorf(`passowrd and confirm password does not match`) return res, fmt.Errorf(`password and confirm password does not match`)
} }
params.Email = strings.ToLower(params.Email) params.Email = strings.ToLower(params.Email)
@@ -35,6 +35,19 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
return res, fmt.Errorf(`invalid email address`) return res, fmt.Errorf(`invalid email address`)
} }
// find user with email
existingUser, err := db.Mgr.GetUserByEmail(params.Email)
if err != nil {
log.Println("user with email " + params.Email + " not found")
}
if existingUser.EmailVerifiedAt != nil {
// email is verified
return res, fmt.Errorf(`%s has already signed up`, params.Email)
} else if existingUser.ID != "" && existingUser.EmailVerifiedAt == nil {
return res, fmt.Errorf("%s has already signed up. please complete the email verification process or reset the password", params.Email)
}
inputRoles := []string{} inputRoles := []string{}
if len(params.Roles) > 0 { if len(params.Roles) > 0 {
@@ -48,16 +61,6 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
inputRoles = constants.DEFAULT_ROLES inputRoles = constants.DEFAULT_ROLES
} }
// find user with email
existingUser, err := db.Mgr.GetUserByEmail(params.Email)
if err != nil {
log.Println("User with email " + params.Email + " not found")
}
if existingUser.EmailVerifiedAt > 0 {
// email is verified
return res, fmt.Errorf(`you have already signed up. Please login`)
}
user := db.User{ user := db.User{
Email: params.Email, Email: params.Email,
} }
@@ -65,45 +68,59 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
user.Roles = strings.Join(inputRoles, ",") user.Roles = strings.Join(inputRoles, ",")
password, _ := utils.HashPassword(params.Password) password, _ := utils.HashPassword(params.Password)
user.Password = password user.Password = &password
if params.FirstName != nil { if params.GivenName != nil {
user.FirstName = *params.FirstName user.GivenName = params.GivenName
} }
if params.LastName != nil { if params.FamilyName != nil {
user.LastName = *params.LastName user.FamilyName = params.FamilyName
} }
user.SignupMethod = enum.BasicAuth.String() if params.MiddleName != nil {
if constants.DISABLE_EMAIL_VERIFICATION == "true" { user.MiddleName = params.MiddleName
user.EmailVerifiedAt = time.Now().Unix()
} }
user, err = db.Mgr.SaveUser(user)
if params.Nickname != nil {
user.Nickname = params.Nickname
}
if params.Gender != nil {
user.Gender = params.Gender
}
if params.Birthdate != nil {
user.Birthdate = params.Birthdate
}
if params.PhoneNumber != nil {
user.PhoneNumber = params.PhoneNumber
}
if params.Picture != nil {
user.Picture = params.Picture
}
user.SignupMethods = enum.BasicAuth.String()
if constants.DISABLE_EMAIL_VERIFICATION {
now := time.Now().Unix()
user.EmailVerifiedAt = &now
}
user, err = db.Mgr.AddUser(user)
if err != nil { if err != nil {
return res, err return res, err
} }
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
userToReturn := &model.User{ userToReturn := utils.GetResponseUserData(user)
ID: userIdStr,
Email: user.Email,
Image: &user.Image,
FirstName: &user.FirstName,
LastName: &user.LastName,
SignupMethod: user.SignupMethod,
EmailVerifiedAt: &user.EmailVerifiedAt,
Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
}
if constants.DISABLE_EMAIL_VERIFICATION != "true" { if !constants.DISABLE_EMAIL_VERIFICATION {
// insert verification request // insert verification request
verificationType := enum.BasicAuthSignup.String() verificationType := enum.BasicAuthSignup.String()
token, err := utils.CreateVerificationToken(params.Email, verificationType) token, err := utils.CreateVerificationToken(params.Email, verificationType)
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,
@@ -128,20 +145,12 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles) accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, accessToken, refreshToken) session.SetToken(userIdStr, accessToken, refreshToken)
go func() { utils.CreateSession(user.ID, gc)
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
}
db.Mgr.SaveSession(sessionData)
}()
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Signed up successfully.`, Message: `Signed up successfully.`,
AccessToken: &accessToken, AccessToken: &accessToken,
AccessTokenExpiresAt: &expiresAt, ExpiresAt: &expiresAt,
User: userToReturn, User: userToReturn,
} }
utils.SetCookie(gc, accessToken) utils.SetCookie(gc, accessToken)

View File

@@ -40,7 +40,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
} }
// validate if all params are not empty // validate if all params are not empty
if params.FirstName == nil && params.LastName == nil && params.Image == nil && params.OldPassword == nil && params.Email == nil { if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.OldPassword == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil {
return res, fmt.Errorf("please enter atleast one param to update") return res, fmt.Errorf("please enter atleast one param to update")
} }
@@ -50,20 +50,40 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
return res, err return res, err
} }
if params.FirstName != nil && user.FirstName != *params.FirstName { if params.GivenName != nil && user.GivenName != params.GivenName {
user.FirstName = *params.FirstName user.GivenName = params.GivenName
} }
if params.LastName != nil && user.LastName != *params.LastName { if params.FamilyName != nil && user.FamilyName != params.FamilyName {
user.LastName = *params.LastName user.FamilyName = params.FamilyName
} }
if params.Image != nil && user.Image != *params.Image { if params.MiddleName != nil && user.MiddleName != params.MiddleName {
user.Image = *params.Image user.MiddleName = params.MiddleName
}
if params.Nickname != nil && user.Nickname != params.Nickname {
user.Nickname = params.Nickname
}
if params.Birthdate != nil && user.Birthdate != params.Birthdate {
user.Birthdate = params.Birthdate
}
if params.Gender != nil && user.Gender != params.Gender {
user.Gender = params.Gender
}
if params.PhoneNumber != nil && user.PhoneNumber != params.PhoneNumber {
user.PhoneNumber = params.PhoneNumber
}
if params.Picture != nil && user.Picture != params.Picture {
user.Picture = params.Picture
} }
if params.OldPassword != nil { if params.OldPassword != nil {
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(*params.OldPassword)); err != nil { if err = bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(*params.OldPassword)); err != nil {
return res, fmt.Errorf("incorrect old password") return res, fmt.Errorf("incorrect old password")
} }
@@ -81,7 +101,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
password, _ := utils.HashPassword(*params.NewPassword) password, _ := utils.HashPassword(*params.NewPassword)
user.Password = password user.Password = &password
} }
hasEmailChanged := false hasEmailChanged := false
@@ -93,7 +113,8 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
} }
newEmail := strings.ToLower(*params.Email) newEmail := strings.ToLower(*params.Email)
// check if user with new email exists // check if user with new email exists
_, err = db.Mgr.GetUserByEmail(newEmail) _, err := db.Mgr.GetUserByEmail(newEmail)
// err = nil means user exists // err = nil means user exists
if err == nil { if err == nil {
return res, fmt.Errorf("user with this email address already exists") return res, fmt.Errorf("user with this email address already exists")
@@ -103,13 +124,13 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
utils.DeleteCookie(gc) utils.DeleteCookie(gc)
user.Email = newEmail user.Email = newEmail
user.EmailVerifiedAt = 0 user.EmailVerifiedAt = nil
hasEmailChanged = true hasEmailChanged = true
// insert verification request // insert verification request
verificationType := enum.UpdateEmail.String() verificationType := enum.UpdateEmail.String()
token, err := utils.CreateVerificationToken(newEmail, verificationType) token, err := utils.CreateVerificationToken(newEmail, verificationType)
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,
@@ -126,7 +147,7 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
_, err = db.Mgr.UpdateUser(user) _, err = db.Mgr.UpdateUser(user)
if err != nil { if err != nil {
log.Println("Error updating user:", err) log.Println("error updating user:", err)
return res, err return res, err
} }
message := `Profile details updated successfully.` message := `Profile details updated successfully.`

View File

@@ -15,7 +15,7 @@ import (
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*model.User, error) { func UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) {
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
var res *model.User var res *model.User
if err != nil { if err != nil {
@@ -26,7 +26,7 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
return res, fmt.Errorf("unauthorized") return res, fmt.Errorf("unauthorized")
} }
if params.FirstName == nil && params.LastName == nil && params.Image == nil && params.Email == nil && params.Roles == nil { if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.Roles == nil {
return res, fmt.Errorf("please enter atleast one param to update") return res, fmt.Errorf("please enter atleast one param to update")
} }
@@ -35,16 +35,36 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
return res, fmt.Errorf(`User not found`) return res, fmt.Errorf(`User not found`)
} }
if params.FirstName != nil && user.FirstName != *params.FirstName { if params.GivenName != nil && user.GivenName != params.GivenName {
user.FirstName = *params.FirstName user.GivenName = params.GivenName
} }
if params.LastName != nil && user.LastName != *params.LastName { if params.FamilyName != nil && user.FamilyName != params.FamilyName {
user.LastName = *params.LastName user.FamilyName = params.FamilyName
} }
if params.Image != nil && user.Image != *params.Image { if params.MiddleName != nil && user.MiddleName != params.MiddleName {
user.Image = *params.Image user.MiddleName = params.MiddleName
}
if params.Nickname != nil && user.Nickname != params.Nickname {
user.Nickname = params.Nickname
}
if params.Birthdate != nil && user.Birthdate != params.Birthdate {
user.Birthdate = params.Birthdate
}
if params.Gender != nil && user.Gender != params.Gender {
user.Gender = params.Gender
}
if params.PhoneNumber != nil && user.PhoneNumber != params.PhoneNumber {
user.PhoneNumber = params.PhoneNumber
}
if params.Picture != nil && user.Picture != params.Picture {
user.Picture = params.Picture
} }
if params.Email != nil && user.Email != *params.Email { if params.Email != nil && user.Email != *params.Email {
@@ -64,12 +84,12 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
utils.DeleteCookie(gc) utils.DeleteCookie(gc)
user.Email = newEmail user.Email = newEmail
user.EmailVerifiedAt = 0 user.EmailVerifiedAt = nil
// insert verification request // insert verification request
verificationType := enum.UpdateEmail.String() verificationType := enum.UpdateEmail.String()
token, err := utils.CreateVerificationToken(newEmail, verificationType) token, err := utils.CreateVerificationToken(newEmail, verificationType)
if err != nil { if err != nil {
log.Println(`Error generating token`, err) log.Println(`error generating token`, err)
} }
db.Mgr.AddVerification(db.VerificationRequest{ db.Mgr.AddVerification(db.VerificationRequest{
Token: token, Token: token,
@@ -110,19 +130,19 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
user, err = db.Mgr.UpdateUser(user) user, err = db.Mgr.UpdateUser(user)
if err != nil { if err != nil {
log.Println("Error updating user:", err) log.Println("error updating user:", err)
return res, err return res, err
} }
res = &model.User{ res = &model.User{
ID: params.ID, ID: params.ID,
Email: user.Email, Email: user.Email,
Image: &user.Image, Picture: user.Picture,
FirstName: &user.FirstName, GivenName: user.GivenName,
LastName: &user.LastName, FamilyName: user.FamilyName,
Roles: strings.Split(user.Roles, ","), Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt, CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt, UpdatedAt: &user.UpdatedAt,
} }
return res, nil return res, nil
} }

View File

@@ -3,7 +3,6 @@ package resolvers
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@@ -27,17 +26,7 @@ func Users(ctx context.Context) ([]*model.User, error) {
} }
for i := 0; i < len(users); i++ { for i := 0; i < len(users); i++ {
res = append(res, &model.User{ res = append(res, utils.GetResponseUserData(users[i]))
ID: fmt.Sprintf("%v", users[i].ID),
Email: users[i].Email,
SignupMethod: users[i].SignupMethod,
FirstName: &users[i].FirstName,
LastName: &users[i].LastName,
EmailVerifiedAt: &users[i].EmailVerifiedAt,
Roles: strings.Split(users[i].Roles, ","),
CreatedAt: &users[i].CreatedAt,
UpdatedAt: &users[i].UpdatedAt,
})
} }
return res, nil return res, nil

View File

@@ -20,7 +20,7 @@ func VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.Aut
return res, err return res, err
} }
_, err = db.Mgr.GetVerificationByToken(params.Token) verificationRequest, err := db.Mgr.GetVerificationByToken(params.Token)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token`) return res, fmt.Errorf(`invalid token`)
} }
@@ -37,43 +37,25 @@ func VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.Aut
} }
// update email_verified_at in users table // update email_verified_at in users table
db.Mgr.UpdateVerificationTime(time.Now().Unix(), user.ID) now := time.Now().Unix()
user.EmailVerifiedAt = &now
db.Mgr.UpdateUser(user)
// delete from verification table // delete from verification table
db.Mgr.DeleteToken(claim.Email) db.Mgr.DeleteVerificationRequest(verificationRequest)
userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles) refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles)
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles) accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, accessToken, refreshToken) session.SetToken(user.ID, accessToken, refreshToken)
go func() { utils.CreateSession(user.ID, gc)
sessionData := db.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
}
db.Mgr.SaveSession(sessionData)
}()
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Email verified successfully.`, Message: `Email verified successfully.`,
AccessToken: &accessToken, AccessToken: &accessToken,
AccessTokenExpiresAt: &expiresAt, ExpiresAt: &expiresAt,
User: &model.User{ User: utils.GetResponseUserData(user),
ID: userIdStr,
Email: user.Email,
Image: &user.Image,
FirstName: &user.FirstName,
LastName: &user.LastName,
SignupMethod: user.SignupMethod,
EmailVerifiedAt: &user.EmailVerifiedAt,
Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
},
} }
utils.SetCookie(gc, accessToken) utils.SetCookie(gc, accessToken)

23
server/router/router.go Normal file
View File

@@ -0,0 +1,23 @@
package router
import (
"github.com/authorizerdev/authorizer/server/handlers"
"github.com/authorizerdev/authorizer/server/middlewares"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin"
)
func InitRouter() *gin.Engine {
router := gin.Default()
router.Use(location.Default())
router.Use(middlewares.GinContextToContextMiddleware())
router.Use(middlewares.CORSMiddleware())
router.GET("/", handlers.PlaygroundHandler())
router.POST("/graphql", handlers.GraphqlHandler())
router.GET("/verify_email", handlers.VerifyEmailHandler())
router.GET("/oauth_login/:oauth_provider", handlers.OAuthLoginHandler())
router.GET("/oauth_callback/:oauth_provider", handlers.OAuthCallbackHandler())
return router
}

View File

@@ -1,7 +1,6 @@
package session package session
import ( import (
"log"
"sync" "sync"
) )
@@ -30,8 +29,6 @@ func (c *InMemoryStore) AddToken(userId, accessToken, refreshToken string) {
c.store[userId] = tempMap c.store[userId] = tempMap
} }
log.Println(c.store)
c.mu.Unlock() c.mu.Unlock()
} }
@@ -41,7 +38,7 @@ func (c *InMemoryStore) DeleteUserSession(userId string) {
c.mu.Unlock() c.mu.Unlock()
} }
func (c *InMemoryStore) DeleteToken(userId, accessToken string) { func (c *InMemoryStore) DeleteVerificationRequest(userId, accessToken string) {
c.mu.Lock() c.mu.Lock()
delete(c.store[userId], accessToken) delete(c.store[userId], accessToken)
c.mu.Unlock() c.mu.Unlock()

View File

@@ -29,7 +29,7 @@ func (c *RedisStore) DeleteUserSession(userId string) {
} }
} }
func (c *RedisStore) DeleteToken(userId, accessToken string) { func (c *RedisStore) DeleteVerificationRequest(userId, accessToken string) {
err := c.store.HDel(c.ctx, "authorizer_"+userId, accessToken).Err() err := c.store.HDel(c.ctx, "authorizer_"+userId, accessToken).Err()
if err != nil { if err != nil {
log.Fatalln("Error deleting redis token:", err) log.Fatalln("Error deleting redis token:", err)
@@ -47,7 +47,7 @@ func (c *RedisStore) GetToken(userId, accessToken string) string {
token := "" token := ""
res, err := c.store.HMGet(c.ctx, "authorizer_"+userId, accessToken).Result() res, err := c.store.HMGet(c.ctx, "authorizer_"+userId, accessToken).Result()
if err != nil { if err != nil {
log.Println("Error getting token from redis store:", err) log.Println("error getting token from redis store:", err)
} }
if len(res) > 0 && res[0] != nil { if len(res) > 0 && res[0] != nil {
token = fmt.Sprintf("%v", res[0]) token = fmt.Sprintf("%v", res[0])
@@ -66,7 +66,7 @@ func (c *RedisStore) GetSocialLoginState(key string) string {
state := "" state := ""
state, err := c.store.Get(c.ctx, key).Result() state, err := c.store.Get(c.ctx, key).Result()
if err != nil { if err != nil {
log.Println("Error getting token from redis store:", err) log.Println("error getting token from redis store:", err)
} }
return state return state

View File

@@ -27,12 +27,12 @@ func SetToken(userId, accessToken, refreshToken string) {
} }
} }
func DeleteToken(userId, accessToken string) { func DeleteVerificationRequest(userId, accessToken string) {
if SessionStoreObj.RedisMemoryStoreObj != nil { if SessionStoreObj.RedisMemoryStoreObj != nil {
SessionStoreObj.RedisMemoryStoreObj.DeleteToken(userId, accessToken) SessionStoreObj.RedisMemoryStoreObj.DeleteVerificationRequest(userId, accessToken)
} }
if SessionStoreObj.InMemoryStoreObj != nil { if SessionStoreObj.InMemoryStoreObj != nil {
SessionStoreObj.InMemoryStoreObj.DeleteToken(userId, accessToken) SessionStoreObj.InMemoryStoreObj.DeleteVerificationRequest(userId, accessToken)
} }
} }
@@ -96,7 +96,7 @@ func RemoveSocialLoginState(key string) {
func InitSession() { func InitSession() {
if constants.REDIS_URL != "" { if constants.REDIS_URL != "" {
log.Println("Using redis store to save sessions") log.Println("using redis store to save sessions")
opt, err := redis.ParseURL(constants.REDIS_URL) opt, err := redis.ParseURL(constants.REDIS_URL)
if err != nil { if err != nil {
log.Fatalln("Error parsing redis url:", err) log.Fatalln("Error parsing redis url:", err)
@@ -114,7 +114,7 @@ func InitSession() {
} }
} else { } else {
log.Println("Using in memory store to save sessions") log.Println("using in memory store to save sessions")
SessionStoreObj.InMemoryStoreObj = &InMemoryStore{ SessionStoreObj.InMemoryStoreObj = &InMemoryStore{
store: map[string]map[string]string{}, store: map[string]map[string]string{},
socialLoginState: map[string]string{}, socialLoginState: map[string]string{},

View File

@@ -26,34 +26,31 @@ func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (st
expiresAt := time.Now().Add(expiryBound).Unix() expiresAt := time.Now().Add(expiryBound).Unix()
resUser := GetResponseUserData(user)
userBytes, _ := json.Marshal(&resUser)
var userMap map[string]interface{}
json.Unmarshal(userBytes, &userMap)
customClaims := jwt.MapClaims{ customClaims := jwt.MapClaims{
"exp": expiresAt, "exp": expiresAt,
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"token_type": tokenType.String(), "token_type": tokenType.String(),
"email": user.Email,
"id": user.ID,
"allowed_roles": strings.Split(user.Roles, ","), "allowed_roles": strings.Split(user.Roles, ","),
constants.JWT_ROLE_CLAIM: roles, constants.JWT_ROLE_CLAIM: roles,
} }
// check for the extra access token script for k, v := range userMap {
if k != "roles" {
customClaims[k] = v
}
}
// check for the extra access token script
accessTokenScript := os.Getenv("CUSTOM_ACCESS_TOKEN_SCRIPT") accessTokenScript := os.Getenv("CUSTOM_ACCESS_TOKEN_SCRIPT")
if accessTokenScript != "" { if accessTokenScript != "" {
userInfo := map[string]interface{}{
"id": user.ID,
"email": user.Email,
"firstName": user.FirstName,
"lastName": user.LastName,
"image": user.Image,
"roles": strings.Split(user.Roles, ","),
"signUpMethods": strings.Split(user.SignupMethod, ","),
}
vm := otto.New() vm := otto.New()
userBytes, _ := json.Marshal(userInfo)
claimBytes, _ := json.Marshal(customClaims)
claimBytes, _ := json.Marshal(customClaims)
vm.Run(fmt.Sprintf(` vm.Run(fmt.Sprintf(`
var user = %s; var user = %s;
var tokenPayload = %s; var tokenPayload = %s;
@@ -64,13 +61,12 @@ func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (st
val, err := vm.Get("functionRes") val, err := vm.Get("functionRes")
if err != nil { if err != nil {
log.Println("=> err custom access token script:", err) log.Println("error getting custom access token script:", err)
} else { } else {
extraPayload := make(map[string]interface{}) extraPayload := make(map[string]interface{})
err = json.Unmarshal([]byte(fmt.Sprintf("%s", val)), &extraPayload) err = json.Unmarshal([]byte(fmt.Sprintf("%s", val)), &extraPayload)
log.Println("extra:", extraPayload)
if err != nil { if err != nil {
log.Println("Error converting accessTokenScript response to map:", err) log.Println("error converting accessTokenScript response to map:", err)
} else { } else {
for k, v := range extraPayload { for k, v := range extraPayload {
customClaims[k] = v customClaims[k] = v

View File

@@ -1,7 +1,6 @@
package utils package utils
import ( import (
"log"
"net/http" "net/http"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@@ -11,17 +10,24 @@ import (
func SetCookie(gc *gin.Context, token string) { func SetCookie(gc *gin.Context, token string) {
secure := true secure := true
httpOnly := true httpOnly := true
host, _ := GetHostParts(constants.AUTHORIZER_URL)
domain := GetDomainName(constants.AUTHORIZER_URL)
if domain != "localhost" {
domain = "." + domain
}
host := GetHostName(constants.AUTHORIZER_URL)
log.Println("=> cookie host", host)
gc.SetSameSite(http.SameSiteNoneMode) gc.SetSameSite(http.SameSiteNoneMode)
gc.SetCookie(constants.COOKIE_NAME, token, 3600, "/", host, secure, httpOnly) gc.SetCookie(constants.COOKIE_NAME, token, 3600, "/", host, secure, httpOnly)
gc.SetCookie(constants.COOKIE_NAME+"-client", token, 3600, "/", domain, secure, httpOnly)
} }
func GetCookie(gc *gin.Context) (string, error) { func GetCookie(gc *gin.Context) (string, error) {
cookie, err := gc.Request.Cookie(constants.COOKIE_NAME) cookie, err := gc.Request.Cookie(constants.COOKIE_NAME)
if err != nil { if err != nil {
return "", err cookie, err = gc.Request.Cookie(constants.COOKIE_NAME + "-client")
if err != nil {
return "", err
}
} }
return cookie.Value, nil return cookie.Value, nil
@@ -31,11 +37,13 @@ func DeleteCookie(gc *gin.Context) {
secure := true secure := true
httpOnly := true httpOnly := true
if !constants.IS_PROD { host, _ := GetHostParts(constants.AUTHORIZER_URL)
secure = false domain := GetDomainName(constants.AUTHORIZER_URL)
if domain != "localhost" {
domain = "." + domain
} }
host := GetHostName(constants.AUTHORIZER_URL)
gc.SetSameSite(http.SameSiteNoneMode) gc.SetSameSite(http.SameSiteNoneMode)
gc.SetCookie(constants.COOKIE_NAME, "", -1, "/", host, secure, httpOnly) gc.SetCookie(constants.COOKIE_NAME, "", -1, "/", host, secure, httpOnly)
gc.SetCookie(constants.COOKIE_NAME+"-client", "", -1, "/", domain, secure, httpOnly)
} }

View File

@@ -0,0 +1,16 @@
package utils
import (
"github.com/authorizerdev/authorizer/server/db"
"github.com/gin-gonic/gin"
)
func CreateSession(userId string, c *gin.Context) {
sessionData := db.Session{
UserID: userId,
UserAgent: GetUserAgent(c.Request),
IP: GetIP(c.Request),
}
db.Mgr.AddSession(sessionData)
}

View File

@@ -1,7 +1,9 @@
package utils package utils
import ( import (
"fmt" "bytes"
"encoding/json"
"html/template"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/email"
@@ -9,16 +11,13 @@ import (
// SendVerificationMail to send verification email // SendVerificationMail to send verification email
func SendVerificationMail(toEmail, token string) error { func SendVerificationMail(toEmail, token string) error {
sender := email.NewSender()
// The receiver needs to be in slice as the receive supports multiple receiver // The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail} Receiver := []string{toEmail}
Subject := "Please verify your email" Subject := "Please verify your email"
message := fmt.Sprintf(` message := `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport"> <meta content="width=device-width, initial-scale=1" name="viewport">
@@ -41,7 +40,6 @@ func SendVerificationMail(toEmail, token string) error {
</xml> </xml>
<![endif]--> <![endif]-->
</head> </head>
<body style="font-family: sans-serif;"> <body style="font-family: sans-serif;">
<div class="es-wrapper-color"> <div class="es-wrapper-color">
<!--[if gte mso 9]> <!--[if gte mso 9]>
@@ -68,14 +66,15 @@ func SendVerificationMail(toEmail, token string) error {
<table width="100%%" cellspacing="0" cellpadding="0"> <table width="100%%" cellspacing="0" cellpadding="0">
<tbody> <tbody>
<tr> <tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank"><img src="%s" alt="icon" style="display: block;" title="icon" width="30"></a></td> <td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.OrgLogo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr> </tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;"> <tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;"> <td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hey there 👋</p> <p>Hey there 👋</p>
<p>We received a request to sign-up for <b>%s</b>. If this is correct, please confirm your email address by clicking the button below.</p> <br/> <p>We have received request to verify email for <b>{{.OrgName}}</b>. If this is correct, please confirm your email address by clicking the button below.</p> <br/>
<a href="%s" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Confirm Email</a> <a
clicktracking="off" href="{{.AuthUrl}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Confirm Email</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -100,10 +99,15 @@ func SendVerificationMail(toEmail, token string) error {
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div> <div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
</body> </body>
</html> </html>
`, constants.ORGANIZATION_LOGO, constants.ORGANIZATION_NAME, constants.AUTHORIZER_URL+"/verify_email"+"?token="+token) `
bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) data := make(map[string]interface{}, 3)
data["OrgLogo"] = constants.ORGANIZATION_LOGO
data["OrgName"] = constants.ORGANIZATION_NAME
data["AuthUrl"] = constants.AUTHORIZER_URL + "/verify_email?token=" + token
message = AddEmailTemplate(message, data, "verify_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
return sender.SendMail(Receiver, Subject, bodyMessage) return email.SendMail(Receiver, Subject, message)
} }
// SendForgotPasswordMail to send verification email // SendForgotPasswordMail to send verification email
@@ -112,17 +116,14 @@ func SendForgotPasswordMail(toEmail, token, host string) error {
constants.RESET_PASSWORD_URL = constants.AUTHORIZER_URL + "/app/reset-password" constants.RESET_PASSWORD_URL = constants.AUTHORIZER_URL + "/app/reset-password"
} }
sender := email.NewSender()
// The receiver needs to be in slice as the receive supports multiple receiver // The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail} Receiver := []string{toEmail}
Subject := "Reset Password" Subject := "Reset Password"
message := fmt.Sprintf(` message := `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport"> <meta content="width=device-width, initial-scale=1" name="viewport">
@@ -145,7 +146,6 @@ func SendForgotPasswordMail(toEmail, token, host string) error {
</xml> </xml>
<![endif]--> <![endif]-->
</head> </head>
<body style="font-family: sans-serif;"> <body style="font-family: sans-serif;">
<div class="es-wrapper-color"> <div class="es-wrapper-color">
<!--[if gte mso 9]> <!--[if gte mso 9]>
@@ -172,14 +172,14 @@ func SendForgotPasswordMail(toEmail, token, host string) error {
<table width="100%%" cellspacing="0" cellpadding="0"> <table width="100%%" cellspacing="0" cellpadding="0">
<tbody> <tbody>
<tr> <tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank"><img src="%s" alt="icon" style="display: block;" title="icon" width="30"></a></td> <td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.OrgLogo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr> </tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;"> <tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;"> <td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hey there 👋</p> <p>Hey there 👋</p>
<p>We received a request to reset password for email: <b>%s</b>. If this is correct, please reset the password clicking the button below.</p> <br/> <p>We have received a request to reset password for email: <b>{{.ToEmail}}</b>. If this is correct, please reset the password clicking the button below.</p> <br/>
<a href="%s" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Reset Password</a> <a clicktracking="off" href="{{.AuthUrl}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Reset Password</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -204,9 +204,28 @@ func SendForgotPasswordMail(toEmail, token, host string) error {
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div> <div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
</body> </body>
</html> </html>
`, constants.ORGANIZATION_LOGO, toEmail, constants.RESET_PASSWORD_URL+"?token="+token) `
bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) data := make(map[string]interface{}, 3)
data["OrgLogo"] = constants.ORGANIZATION_LOGO
data["ToEmail"] = constants.ORGANIZATION_NAME
data["AuthUrl"] = constants.RESET_PASSWORD_URL + "?token=" + token
message = AddEmailTemplate(message, data, "reset_password_email.tmpl")
return sender.SendMail(Receiver, Subject, bodyMessage) return email.SendMail(Receiver, Subject, message)
}
func AddEmailTemplate(a string, b map[string]interface{}, templateName string) string {
tmpl, err := template.New(templateName).Parse(a)
if err != nil {
output, _ := json.Marshal(b)
return string(output)
}
buf := &bytes.Buffer{}
err = tmpl.Execute(buf, b)
if err != nil {
panic(err)
}
s := buf.String()
return s
} }

View File

@@ -0,0 +1,32 @@
package utils
import (
"strings"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
)
func GetResponseUserData(user db.User) *model.User {
isEmailVerified := user.EmailVerifiedAt != nil
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
return &model.User{
ID: user.ID,
Email: user.Email,
EmailVerified: isEmailVerified,
SignupMethods: user.SignupMethods,
GivenName: user.GivenName,
FamilyName: user.FamilyName,
MiddleName: user.MiddleName,
Nickname: user.Nickname,
PreferredUsername: &user.Email,
Gender: user.Gender,
Birthdate: user.Birthdate,
PhoneNumber: user.PhoneNumber,
PhoneNumberVerified: &isPhoneVerified,
Picture: user.Picture,
Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
}
}

Some files were not shown because too many files have changed in this diff Show More