Compare commits

...

14 Commits

Author SHA1 Message Date
Lakhan Samani
74a8024131 feat: add integration test for invite_member 2022-03-15 12:09:54 +05:30
Lakhan Samani
5e6ee8d9b0 fix: setup-password flow 2022-03-15 09:57:09 +05:30
Lakhan Samani
3e7150f872 fix: redirect uri 2022-03-15 09:56:50 +05:30
Lakhan Samani
9a19552f72 feat: add resolver for inviting members 2022-03-15 08:53:48 +05:30
Lakhan Samani
1b387f7564 fix: getting version in meta api 2022-03-09 18:55:18 +05:30
Lakhan Samani
8e79ab77b2 Merge pull request #131 from authorizerdev/feat/open-id
Add open id authorization flow with PKCE
2022-03-09 17:27:16 +05:30
Lakhan Samani
2bf6b8f91d fix: remove log 2022-03-09 17:24:53 +05:30
Lakhan Samani
776c0fba8b chore: app dependencies 2022-03-09 17:21:55 +05:30
Lakhan Samani
dd64aa2e79 feat: add version info 2022-03-09 11:53:34 +05:30
Lakhan Samani
157b13baa7 fix: basic auth redirect 2022-03-09 10:10:39 +05:30
Lakhan Samani
d1e284116d fix: verification request model 2022-03-09 07:10:07 +05:30
Lakhan Samani
2f9725d8e1 fix: verification request 2022-03-09 06:41:38 +05:30
Lakhan Samani
ee7aea7bee fix: verify email 2022-03-08 22:55:45 +05:30
Lakhan Samani
5d73df0040 fix: magic link login 2022-03-08 22:41:33 +05:30
35 changed files with 632 additions and 56 deletions

30
app/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "0.9.0-beta.2", "@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",
@@ -24,9 +24,9 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "0.4.0-beta.0", "version": "0.4.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
"integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==", "integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
}, },
@@ -35,11 +35,11 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "0.9.0-beta.2", "version": "0.9.0-beta.7",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
"integrity": "sha512-clngw7MdFzvnns9rgrg9fHRH4p3K+HGGMju6qhdjDF+4vPruSu6HwBi1hRvVxLi1q7CZ25CEE7CfA7Vfq7H3Bw==", "integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^0.4.0-beta.0", "@authorizerdev/authorizer-js": "^0.4.0-beta.3",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
@@ -829,19 +829,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "0.4.0-beta.0", "version": "0.4.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
"integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==", "integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
"requires": { "requires": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "0.9.0-beta.2", "version": "0.9.0-beta.7",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
"integrity": "sha512-clngw7MdFzvnns9rgrg9fHRH4p3K+HGGMju6qhdjDF+4vPruSu6HwBi1hRvVxLi1q7CZ25CEE7CfA7Vfq7H3Bw==", "integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.4.0-beta.0", "@authorizerdev/authorizer-js": "^0.4.0-beta.3",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"

View File

@@ -11,7 +11,7 @@
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "0.9.0-beta.2", "@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",

View File

@@ -2,10 +2,33 @@ import React from 'react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { AuthorizerProvider } from '@authorizerdev/authorizer-react'; import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
import Root from './Root'; import Root from './Root';
import { createRandomString } from './utils/common';
export default function App() { export default function App() {
const searchParams = new URLSearchParams(window.location.search);
const state = searchParams.get('state') || createRandomString();
const scope = searchParams.get('scope')
? searchParams.get('scope')?.toString().split(' ')
: `openid profile email`;
const urlProps: Record<string, any> = {
state,
scope,
};
const redirectURL =
searchParams.get('redirect_uri') || searchParams.get('redirectURL');
if (redirectURL) {
urlProps.redirectURL = redirectURL;
} else {
urlProps.redirectURL = window.location.origin + '/app';
}
const globalState: Record<string, string> = {
// @ts-ignore // @ts-ignore
const globalState: Record<string, string> = window['__authorizer__']; ...window['__authorizer__'],
...urlProps,
};
return ( return (
<div <div
style={{ style={{
@@ -38,7 +61,7 @@ export default function App() {
redirectURL: globalState.redirectURL, redirectURL: globalState.redirectURL,
}} }}
> >
<Root /> <Root globalState={globalState} />
</AuthorizerProvider> </AuthorizerProvider>
</BrowserRouter> </BrowserRouter>
</div> </div>

View File

@@ -1,19 +1,26 @@
import React, { useEffect, lazy, Suspense } 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 SetupPassword from './pages/setup-password';
const ResetPassword = lazy(() => import('./pages/rest-password')); const ResetPassword = lazy(() => import('./pages/rest-password'));
const Login = lazy(() => import('./pages/login')); const Login = lazy(() => import('./pages/login'));
const Dashboard = lazy(() => import('./pages/dashboard')); const Dashboard = lazy(() => import('./pages/dashboard'));
export default function Root() { export default function Root({
globalState,
}: {
globalState: Record<string, string>;
}) {
const { token, loading, config } = useAuthorizer(); const { token, loading, config } = useAuthorizer();
useEffect(() => { useEffect(() => {
if (token) { if (token) {
console.log({ token });
let redirectURL = config.redirectURL || '/app'; let redirectURL = config.redirectURL || '/app';
const params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&refresh_token=${token.refresh_token}`; let params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&state=${globalState.state}`;
if (token.refresh_token) {
params += `&refresh_token=${token.refresh_token}`;
}
const url = new URL(redirectURL); const url = new URL(redirectURL);
if (redirectURL.includes('?')) { if (redirectURL.includes('?')) {
redirectURL = `${redirectURL}&${params}`; redirectURL = `${redirectURL}&${params}`;
@@ -54,6 +61,9 @@ export default function Root() {
<Route path="/app/reset-password"> <Route path="/app/reset-password">
<ResetPassword /> <ResetPassword />
</Route> </Route>
<Route path="/app/setup-password">
<SetupPassword />
</Route>
</Switch> </Switch>
</Suspense> </Suspense>
); );

View File

@@ -0,0 +1,12 @@
import React, { Fragment } from 'react';
import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react';
export default function SetupPassword() {
return (
<Fragment>
<h1 style={{ textAlign: 'center' }}>Setup new Password</h1>
<br />
<AuthorizerResetPassword />
</Fragment>
);
}

22
app/src/utils/common.ts Normal file
View File

@@ -0,0 +1,22 @@
export const getCrypto = () => {
//ie 11.x uses msCrypto
return (window.crypto || (window as any).msCrypto) as Crypto;
};
export const createRandomString = () => {
const charset =
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.';
let random = '';
const randomValues = Array.from(
getCrypto().getRandomValues(new Uint8Array(43))
);
randomValues.forEach((v) => (random += charset[v % charset.length]));
return random;
};
export const createQueryParams = (params: any) => {
return Object.keys(params)
.filter((k) => typeof params[k] !== 'undefined')
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
};

View File

@@ -29,10 +29,11 @@ import {
} from 'react-icons/fi'; } from 'react-icons/fi';
import { IconType } from 'react-icons'; import { IconType } from 'react-icons';
import { ReactText } from 'react'; import { ReactText } from 'react';
import { useMutation } from 'urql'; import { useMutation, useQuery } from 'urql';
import { NavLink, useNavigate, useLocation } from 'react-router-dom'; import { NavLink, useNavigate, useLocation } from 'react-router-dom';
import { useAuthContext } from '../contexts/AuthContext'; import { useAuthContext } from '../contexts/AuthContext';
import { AdminLogout } from '../graphql/mutation'; import { AdminLogout } from '../graphql/mutation';
import { MetaQuery } from '../graphql/queries';
interface LinkItemProps { interface LinkItemProps {
name: string; name: string;
@@ -51,6 +52,7 @@ interface SidebarProps extends BoxProps {
export const Sidebar = ({ onClose, ...rest }: SidebarProps) => { export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const [{ fetching, data }] = useQuery({ query: MetaQuery });
return ( return (
<Box <Box
transition="3s ease" transition="3s ease"
@@ -98,6 +100,19 @@ export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
> >
<NavItem icon={FiCode}>API Playground</NavItem> <NavItem icon={FiCode}>API Playground</NavItem>
</Link> </Link>
{data?.meta?.version && (
<Text
color="gray.600"
fontSize="sm"
textAlign="center"
position="absolute"
bottom="5"
left="7"
>
Current Version: {data.meta.version}
</Text>
)}
</Box> </Box>
); );
}; };

View File

@@ -1,3 +1,12 @@
export const MetaQuery = `
query MetaQuery {
meta {
version
client_id
}
}
`;
export const AdminSessionQuery = ` export const AdminSessionQuery = `
query { query {
_admin_session{ _admin_session{

View File

@@ -1,8 +1,10 @@
import { Box, Center, Flex, Image, Text } from '@chakra-ui/react'; import { Box, Flex, Image, Text, Spinner } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { LOGO_URL } from '../constants'; import { useQuery } from 'urql';
import { MetaQuery } from '../graphql/queries';
export function AuthLayout({ children }: { children: React.ReactNode }) { export function AuthLayout({ children }: { children: React.ReactNode }) {
const [{ fetching, data }] = useQuery({ query: MetaQuery });
return ( return (
<Flex <Flex
flexWrap="wrap" flexWrap="wrap"
@@ -23,9 +25,18 @@ export function AuthLayout({ children }: { children: React.ReactNode }) {
</Text> </Text>
</Flex> </Flex>
{fetching ? (
<Spinner />
) : (
<>
<Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl"> <Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl">
{children} {children}
</Box> </Box>
<Text color="gray.600" fontSize="sm">
Current Version: {data.meta.version}
</Text>
</>
)}
</Flex> </Flex>
); );
} }

View File

@@ -6,7 +6,6 @@ import {
useToast, useToast,
VStack, VStack,
Text, Text,
Divider,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useMutation } from 'urql'; import { useMutation } from 'urql';

View File

@@ -1,5 +1,7 @@
package constants package constants
var VERSION = "0.0.1"
const ( const (
// Envstore identifier // Envstore identifier
// StringStore string store identifier // StringStore string store identifier
@@ -13,8 +15,6 @@ const (
EnvKeyEnv = "ENV" EnvKeyEnv = "ENV"
// EnvKeyEnvPath key for cli arg variable ENV_PATH // EnvKeyEnvPath key for cli arg variable ENV_PATH
EnvKeyEnvPath = "ENV_PATH" EnvKeyEnvPath = "ENV_PATH"
// EnvKeyVersion key for build arg version
EnvKeyVersion = "VERSION"
// EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL // EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL
// TODO: remove support AUTHORIZER_URL env // TODO: remove support AUTHORIZER_URL env
EnvKeyAuthorizerURL = "AUTHORIZER_URL" EnvKeyAuthorizerURL = "AUTHORIZER_URL"

View File

@@ -12,7 +12,7 @@ type VerificationRequest struct {
CreatedAt int64 `json:"created_at" bson:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"` Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
Nonce string `gorm:"type:char(36)" json:"nonce" bson:"nonce"` Nonce string `gorm:"type:text" json:"nonce" bson:"nonce"`
RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"` RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"`
} }

View File

@@ -21,7 +21,7 @@ func (p *provider) AddVerificationRequest(verificationRequest models.Verificatio
verificationRequest.UpdatedAt = time.Now().Unix() verificationRequest.UpdatedAt = time.Now().Unix()
result := p.db.Clauses(clause.OnConflict{ result := p.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}}, Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}), DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at", "nonce", "redirect_uri"}),
}).Create(&verificationRequest) }).Create(&verificationRequest)
if result.Error != nil { if result.Error != nil {

View File

@@ -0,0 +1,113 @@
package email
import (
"log"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
)
// InviteEmail to send invite email
func InviteEmail(toEmail, token, url string) error {
// The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail}
Subject := "Please accept the invitation"
message := `
<!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">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="telephone=no" name="format-detection">
<title></title>
<!--[if (mso 16)]>
<style type="text/css">
a {}
</style>
<![endif]-->
<!--[if gte mso 9]><style>sup { font-size: 100%% !important; }</style><![endif]-->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG></o:AllowPNG>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body style="font-family: sans-serif;">
<div class="es-wrapper-color">
<!--[if gte mso 9]>
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
<v:fill type="tile" color="#ffffff"></v:fill>
</v:background>
<![endif]-->
<table class="es-wrapper" width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-email-paddings" valign="top">
<table class="es-content esd-footer-popover" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td class="esd-stripe" align="center">
<table class="es-content-body" style="border-left:1px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-bottom:1px solid transparent;padding:20px 0px;" width="600" cellspacing="0" cellpadding="0" bgcolor="#ffffff" align="center">
<tbody>
<tr>
<td class="esd-structure es-p20t es-p40b es-p40r es-p40l" esd-custom-block-id="8537" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-container-frame" width="518" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<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="{{.org_logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr>
<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;">
<p>Hi there 👋</p>
<p>Join us! You are invited to sign-up for <b>{{.org_name}}</b>. Please accept the invitation by clicking the clicking the button below.</p> <br/>
<a
clicktracking="off" href="{{.verification_url}}" 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;">Get Started</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
</body>
</html>
`
data := make(map[string]interface{}, 3)
data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
data["verification_url"] = url + "?token=" + token
message = addEmailTemplate(message, data, "invite_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
err := SendMail(Receiver, Subject, message)
if err != nil {
log.Println("=> error sending email:", err)
}
return err
}

View File

@@ -1,6 +1,8 @@
package email package email
import ( import (
"log"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
) )
@@ -103,5 +105,9 @@ func SendVerificationMail(toEmail, token, hostname string) error {
message = addEmailTemplate(message, data, "verify_email.tmpl") message = addEmailTemplate(message, data, "verify_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
return SendMail(Receiver, Subject, message) err := SendMail(Receiver, Subject, message)
if err != nil {
log.Println("=> error sending email:", err)
}
return err
} }

View File

@@ -114,6 +114,7 @@ type ComplexityRoot struct {
AdminSignup func(childComplexity int, params model.AdminSignupInput) int AdminSignup func(childComplexity int, params model.AdminSignupInput) int
DeleteUser func(childComplexity int, params model.DeleteUserInput) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int
ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int
InviteMembers func(childComplexity int, params model.InviteMemberInput) int
Login func(childComplexity int, params model.LoginInput) int Login func(childComplexity int, params model.LoginInput) int
Logout func(childComplexity int) int Logout func(childComplexity int) int
MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int
@@ -208,6 +209,7 @@ type MutationResolver interface {
AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error) AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error)
AdminLogout(ctx context.Context) (*model.Response, error) AdminLogout(ctx context.Context) (*model.Response, error)
UpdateEnv(ctx context.Context, params model.UpdateEnvInput) (*model.Response, error) UpdateEnv(ctx context.Context, params model.UpdateEnvInput) (*model.Response, error)
InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error)
} }
type QueryResolver interface { type QueryResolver interface {
Meta(ctx context.Context) (*model.Meta, error) Meta(ctx context.Context) (*model.Meta, error)
@@ -660,6 +662,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true
case "Mutation._invite_members":
if e.complexity.Mutation.InviteMembers == nil {
break
}
args, err := ec.field_Mutation__invite_members_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.InviteMembers(childComplexity, args["params"].(model.InviteMemberInput)), true
case "Mutation.login": case "Mutation.login":
if e.complexity.Mutation.Login == nil { if e.complexity.Mutation.Login == nil {
break break
@@ -1343,6 +1357,7 @@ input SignUpInput {
password: String! password: String!
confirm_password: String! confirm_password: String!
roles: [String!] roles: [String!]
scope: [String!]
} }
input LoginInput { input LoginInput {
@@ -1433,6 +1448,11 @@ input OAuthRevokeInput {
refresh_token: String! refresh_token: String!
} }
input InviteMemberInput {
emails: [String!]!
redirect_uri: String
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -1451,6 +1471,7 @@ type Mutation {
_admin_login(params: AdminLoginInput!): Response! _admin_login(params: AdminLoginInput!): Response!
_admin_logout: Response! _admin_logout: Response!
_update_env(params: UpdateEnvInput!): Response! _update_env(params: UpdateEnvInput!): Response!
_invite_members(params: InviteMemberInput!): Response!
} }
type Query { type Query {
@@ -1516,6 +1537,21 @@ func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 model.InviteMemberInput
if tmp, ok := rawArgs["params"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
arg0, err = ec.unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["params"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation__update_env_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation__update_env_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@@ -4181,6 +4217,48 @@ func (ec *executionContext) _Mutation__update_env(ctx context.Context, field gra
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation__invite_members(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation__invite_members_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().InviteMembers(rctx, args["params"].(model.InviteMemberInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*model.Response)
fc.Result = res
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) { func (ec *executionContext) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -6913,6 +6991,37 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, obj interface{}) (model.InviteMemberInput, error) {
var it model.InviteMemberInput
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
for k, v := range asMap {
switch k {
case "emails":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("emails"))
it.Emails, err = ec.unmarshalNString2ᚕstringᚄ(ctx, v)
if err != nil {
return it, err
}
case "redirect_uri":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj interface{}) (model.LoginInput, error) { func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj interface{}) (model.LoginInput, error) {
var it model.LoginInput var it model.LoginInput
asMap := map[string]interface{}{} asMap := map[string]interface{}{}
@@ -7298,6 +7407,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i
if err != nil { if err != nil {
return it, err return it, err
} }
case "scope":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("scope"))
it.Scope, err = ec.unmarshalOString2ᚕstringᚄ(ctx, v)
if err != nil {
return it, err
}
} }
} }
@@ -8173,6 +8290,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "_invite_members":
out.Values[i] = ec._Mutation__invite_members(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -8902,6 +9024,11 @@ func (ec *executionContext) marshalNInt642int64(ctx context.Context, sel ast.Sel
return res return res
} }
func (ec *executionContext) unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx context.Context, v interface{}) (model.InviteMemberInput, error) {
res, err := ec.unmarshalInputInviteMemberInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐLoginInput(ctx context.Context, v interface{}) (model.LoginInput, error) { func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐLoginInput(ctx context.Context, v interface{}) (model.LoginInput, error) {
res, err := ec.unmarshalInputLoginInput(ctx, v) res, err := ec.unmarshalInputLoginInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -74,6 +74,11 @@ type ForgotPasswordInput struct {
RedirectURI *string `json:"redirect_uri"` RedirectURI *string `json:"redirect_uri"`
} }
type InviteMemberInput struct {
Emails []string `json:"emails"`
RedirectURI *string `json:"redirect_uri"`
}
type LoginInput struct { type LoginInput struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
@@ -153,6 +158,7 @@ type SignUpInput struct {
Password string `json:"password"` Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"` ConfirmPassword string `json:"confirm_password"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Scope []string `json:"scope"`
} }
type UpdateEnvInput struct { type UpdateEnvInput struct {

View File

@@ -181,6 +181,7 @@ input SignUpInput {
password: String! password: String!
confirm_password: String! confirm_password: String!
roles: [String!] roles: [String!]
scope: [String!]
} }
input LoginInput { input LoginInput {
@@ -271,6 +272,11 @@ input OAuthRevokeInput {
refresh_token: String! refresh_token: String!
} }
input InviteMemberInput {
emails: [String!]!
redirect_uri: String
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -289,6 +295,7 @@ type Mutation {
_admin_login(params: AdminLoginInput!): Response! _admin_login(params: AdminLoginInput!): Response!
_admin_logout: Response! _admin_logout: Response!
_update_env(params: UpdateEnvInput!): Response! _update_env(params: UpdateEnvInput!): Response!
_invite_members(params: InviteMemberInput!): Response!
} }
type Query { type Query {

View File

@@ -75,6 +75,10 @@ func (r *mutationResolver) UpdateEnv(ctx context.Context, params model.UpdateEnv
return resolvers.UpdateEnvResolver(ctx, params) return resolvers.UpdateEnvResolver(ctx, params)
} }
func (r *mutationResolver) InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) {
return resolvers.InviteMembersResolver(ctx, params)
}
func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) {
return resolvers.MetaResolver(ctx) return resolvers.MetaResolver(ctx)
} }
@@ -109,7 +113,5 @@ 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 ( type mutationResolver struct{ *Resolver }
mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
queryResolver struct{ *Resolver }
)

View File

@@ -13,7 +13,7 @@ import (
// Handler to logout user // Handler to logout user
func LogoutHandler() gin.HandlerFunc { func LogoutHandler() gin.HandlerFunc {
return func(gc *gin.Context) { return func(gc *gin.Context) {
redirectURL := strings.TrimSpace(gc.Query("redirect_url")) redirectURL := strings.TrimSpace(gc.Query("redirect_uri"))
// get fingerprint hash // get fingerprint hash
fingerprintHash, err := cookie.GetSession(gc) fingerprintHash, err := cookie.GetSession(gc)
if err != nil { if err != nil {
@@ -37,7 +37,7 @@ func LogoutHandler() gin.HandlerFunc {
cookie.DeleteSession(gc) cookie.DeleteSession(gc)
if redirectURL != "" { if redirectURL != "" {
gc.Redirect(http.StatusPermanentRedirect, redirectURL) gc.Redirect(http.StatusFound, redirectURL)
} else { } else {
gc.JSON(http.StatusOK, gin.H{ gc.JSON(http.StatusOK, gin.H{
"message": "Logged out successfully", "message": "Logged out successfully",

View File

@@ -95,7 +95,7 @@ func VerifyEmailHandler() gin.HandlerFunc {
} }
if redirectURL == "" { if redirectURL == "" {
redirectURL = claim["redirect_url"].(string) redirectURL = claim["redirect_uri"].(string)
} }
if strings.Contains(redirectURL, "?") { if strings.Contains(redirectURL, "?") {

View File

@@ -21,7 +21,8 @@ func main() {
envstore.ARG_ENV_FILE = flag.String("env_file", "", "Env file path") envstore.ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
flag.Parse() flag.Parse()
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, VERSION) log.Println("=> version:", VERSION)
constants.VERSION = VERSION
// initialize required envs (mainly db & env file path) // initialize required envs (mainly db & env file path)
err := env.InitRequiredEnv() err := env.InitRequiredEnv()

View File

@@ -43,7 +43,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
if err != nil { if err != nil {
return res, err return res, err
} }
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) redirectURL := utils.GetAppURL(gc) + "/reset-password"
if params.RedirectURI != nil { if params.RedirectURI != nil {
redirectURL = *params.RedirectURI redirectURL = *params.RedirectURI
} }

View File

@@ -0,0 +1,135 @@
package resolvers
import (
"context"
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
emailservice "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
)
// InviteMembersResolver resolver to invite members
func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
return nil, err
}
if !token.IsSuperAdmin(gc) {
return nil, errors.New("unauthorized")
}
// this feature is only allowed if email server is configured
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
return nil, errors.New("email sending is disabled")
}
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) && envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
return nil, errors.New("either basic authentication or magic link login is required")
}
// filter valid emails
emails := []string{}
for _, email := range params.Emails {
if utils.IsValidEmail(email) {
emails = append(emails, email)
}
}
if len(emails) == 0 {
return nil, errors.New("no valid emails found")
}
// TODO: optimise to use like query instead of looping through emails and getting user individually
// for each emails check if emails exists in db
newEmails := []string{}
for _, email := range emails {
_, err := db.Provider.GetUserByEmail(email)
if err != nil {
log.Printf("%s user not found. inviting user.", email)
newEmails = append(newEmails, email)
} else {
log.Println("%s user already exists. skipping.", email)
}
}
if len(newEmails) == 0 {
return nil, errors.New("all emails already exist")
}
// invite new emails
for _, email := range newEmails {
user := models.User{
Email: email,
Roles: strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ","),
}
hostname := utils.GetHost(gc)
verifyEmailURL := hostname + "/verify_email"
appURL := utils.GetAppURL(gc)
redirectURL := appURL
if params.RedirectURI != nil {
redirectURL = *params.RedirectURI
}
_, nonceHash, err := utils.GenerateNonce()
if err != nil {
return nil, err
}
verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL)
if err != nil {
log.Println(`error generating token`, err)
}
verificationRequest := models.VerificationRequest{
Token: verificationToken,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: email,
Nonce: nonceHash,
RedirectURI: redirectURL,
}
// use magic link login if that option is on
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
user.SignupMethods = constants.SignupMethodMagicLinkLogin
verificationRequest.Identifier = constants.VerificationTypeMagicLinkLogin
} else {
// use basic authentication if that option is on
user.SignupMethods = constants.SignupMethodBasicAuth
verificationRequest.Identifier = constants.VerificationTypeForgotPassword
verifyEmailURL = appURL + "/setup-password"
}
user, err = db.Provider.AddUser(user)
if err != nil {
log.Printf("error inviting user: %s, err: %v", email, err)
return nil, err
}
_, err = db.Provider.AddVerificationRequest(verificationRequest)
if err != nil {
log.Printf("error inviting user: %s, err: %v", email, err)
return nil, err
}
go emailservice.InviteEmail(email, verificationToken, verifyEmailURL)
}
return &model.Response{
Message: fmt.Sprintf("%d user(s) invited successfully.", len(newEmails)),
}, nil
}

View File

@@ -123,7 +123,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
if params.Scope != nil && len(params.Scope) > 0 { if params.Scope != nil && len(params.Scope) > 0 {
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ") redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")
} }
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) redirectURL := utils.GetAppURL(gc)
if params.RedirectURI != nil { if params.RedirectURI != nil {
redirectURL = *params.RedirectURI redirectURL = *params.RedirectURI
} }
@@ -139,7 +139,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ _, err = db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: verificationToken, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
@@ -147,8 +147,11 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
Nonce: nonceHash, Nonce: nonceHash,
RedirectURI: redirectURL, RedirectURI: redirectURL,
}) })
if err != nil {
return res, err
}
// exec it as go routin so that we can reduce the api latency // exec it as go routing so that we can reduce the api latency
go email.SendVerificationMail(params.Email, verificationToken, hostname) go email.SendVerificationMail(params.Email, verificationToken, hostname)
} }

View File

@@ -2,7 +2,9 @@ package resolvers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log"
"github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
@@ -24,13 +26,15 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
sessionToken, err := cookie.GetSession(gc) sessionToken, err := cookie.GetSession(gc)
if err != nil { if err != nil {
return res, err log.Println("error getting session token:", err)
return res, errors.New("unauthorized")
} }
// get session from cookie // get session from cookie
claims, err := token.ValidateBrowserSession(gc, sessionToken) claims, err := token.ValidateBrowserSession(gc, sessionToken)
if err != nil { if err != nil {
return res, err log.Println("session validation failed:", err)
return res, errors.New("unauthorized")
} }
userID := claims.Subject userID := claims.Subject
user, err := db.Provider.GetUserByID(userID) user, err := db.Provider.GetUserByID(userID)

View File

@@ -128,7 +128,7 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
return res, err return res, err
} }
verificationType := constants.VerificationTypeBasicAuthSignup verificationType := constants.VerificationTypeBasicAuthSignup
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL) verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
return res, err return res, err
@@ -151,6 +151,9 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
} }
} else { } else {
scope := []string{"openid", "email", "profile"} scope := []string{"openid", "email", "profile"}
if params.Scope != nil && len(scope) > 0 {
scope = params.Scope
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope) authToken, err := token.CreateAuthToken(gc, user, roles, scope)
if err != nil { if err != nil {

View File

@@ -134,7 +134,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
return res, err return res, err
} }
verificationType := constants.VerificationTypeUpdateEmail verificationType := constants.VerificationTypeUpdateEmail
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL) verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)

View File

@@ -106,7 +106,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, err return res, err
} }
verificationType := constants.VerificationTypeUpdateEmail verificationType := constants.VerificationTypeUpdateEmail
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) redirectURL := utils.GetAppURL(gc)
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL) verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)

View File

@@ -0,0 +1,58 @@
package test
import (
"fmt"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func inviteUserTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run(`should invite user successfully`, func(t *testing.T) {
req, ctx := createContext(s)
emails := []string{"invite_member1." + s.TestInfo.Email}
// unauthorized error
res, err := resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
Emails: emails,
})
assert.Error(t, err)
assert.Nil(t, res)
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
// invalid emails test
invalidEmailsTest := []string{
"test",
"test.com",
}
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
Emails: invalidEmailsTest,
})
// valid test
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
Emails: emails,
})
assert.Nil(t, err)
assert.NotNil(t, res)
// duplicate error test
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
Emails: emails,
})
assert.Error(t, err)
assert.Nil(t, res)
cleanData(emails[0])
})
}

View File

@@ -15,7 +15,7 @@ func TestResolvers(t *testing.T) {
// constants.DbTypeArangodb: "http://localhost:8529", // constants.DbTypeArangodb: "http://localhost:8529",
// constants.DbTypeMongodb: "mongodb://localhost:27017", // constants.DbTypeMongodb: "mongodb://localhost:27017",
} }
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, "test")
for dbType, dbURL := range databases { for dbType, dbURL := range databases {
s := testSetup() s := testSetup()
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL) envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL)
@@ -62,6 +62,7 @@ func TestResolvers(t *testing.T) {
magicLinkLoginTests(t, s) magicLinkLoginTests(t, s)
logoutTests(t, s) logoutTests(t, s)
metaTests(t, s) metaTests(t, s)
inviteUserTest(t, s)
}) })
} }
} }

View File

@@ -2,7 +2,6 @@ package token
import ( import (
"errors" "errors"
"fmt"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/crypto"
@@ -92,7 +91,6 @@ func ParseJWTToken(token, hostname, nonce, subject string) (jwt.MapClaims, error
return claims, errors.New("invalid audience") return claims, errors.New("invalid audience")
} }
fmt.Println("claims:", claims, claims["nonce"], nonce)
if claims["nonce"] != nonce { if claims["nonce"] != nonce {
return claims, errors.New("invalid nonce") return claims, errors.New("invalid nonce")
} }

View File

@@ -18,7 +18,7 @@ func CreateVerificationToken(email, tokenType, hostname, nonceHash, redirectURL
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"token_type": tokenType, "token_type": tokenType,
"nonce": nonceHash, "nonce": nonceHash,
"redirect_url": redirectURL, "redirect_uri": redirectURL,
} }
return SignJWTToken(claims) return SignJWTToken(claims)

View File

@@ -9,7 +9,7 @@ import (
// GetMeta helps in getting the meta data about the deployment from EnvData // GetMeta helps in getting the meta data about the deployment from EnvData
func GetMetaInfo() model.Meta { func GetMetaInfo() model.Meta {
return model.Meta{ return model.Meta{
Version: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyVersion), Version: constants.VERSION,
ClientID: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID), ClientID: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
IsGoogleLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret) != "", IsGoogleLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret) != "",
IsGithubLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret) != "", IsGithubLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret) != "",

View File

@@ -4,6 +4,8 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -71,3 +73,12 @@ func GetDomainName(uri string) string {
return host return host
} }
// GetAppURL to get /app/ url if not configured by user
func GetAppURL(gc *gin.Context) string {
envAppURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
if envAppURL == "" {
envAppURL = GetHost(gc) + "/app"
}
return envAppURL
}