Merge pull request #104 from jyash97/yash/dashboard

feat: setup authorizer dashboard
This commit is contained in:
Lakhan Samani 2022-01-17 11:01:19 +05:30 committed by GitHub
commit 7ce96367a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 6025 additions and 329 deletions

View File

@ -23,7 +23,8 @@ jobs:
sudo mv github-assets-uploader /usr/sbin/ && \ sudo mv github-assets-uploader /usr/sbin/ && \
sudo rm -f github-assets-uploader.tar.gz && \ sudo rm -f github-assets-uploader.tar.gz && \
github-assets-uploader -version && \ github-assets-uploader -version && \
make build-app make build-app && \
make build-dashboard
- name: Print Go paths - name: Print Go paths
run: whereis go run: whereis go
- name: Print Go Version - name: Print Go Version
@ -37,12 +38,12 @@ jobs:
make clean && \ make clean && \
CGO_ENABLED=1 GOOS=windows CC=/usr/bin/x86_64-w64-mingw32-gcc make && \ CGO_ENABLED=1 GOOS=windows CC=/usr/bin/x86_64-w64-mingw32-gcc make && \
mv build/server build/server.exe && \ mv build/server build/server.exe && \
zip -vr authorizer-${VERSION}-windows-amd64.zip .env app/build build templates zip -vr authorizer-${VERSION}-windows-amd64.zip .env app/build build templates dashboard/build
- name: Package files for linux - name: Package files for linux
run: | run: |
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 dashboard/build
- 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} && \

2
.gitignore vendored
View File

@ -3,6 +3,8 @@ server/.env
data data
app/node_modules app/node_modules
app/build app/build
dashboard/node_modules
dashboard/build
build build
.env .env
data.db data.db

View File

@ -14,14 +14,17 @@ RUN apk add build-base &&\
FROM node:17-alpine3.12 as node-builder FROM node:17-alpine3.12 as node-builder
WORKDIR /authorizer WORKDIR /authorizer
COPY app app COPY app app
COPY dashboard dashboard
COPY Makefile . COPY Makefile .
RUN apk add build-base &&\ RUN apk add build-base &&\
make build-app make build-app && \
make build-dashboard
FROM alpine:latest FROM alpine:latest
WORKDIR /root/ WORKDIR /root/
RUN mkdir app RUN mkdir app dashboard
COPY --from=node-builder /authorizer/app/build app/build COPY --from=node-builder /authorizer/app/build app/build
COPY --from=node-builder /authorizer/dashboard/build dashboard/build
COPY --from=go-builder /authorizer/build build COPY --from=go-builder /authorizer/build build
COPY templates templates COPY templates templates
EXPOSE 8080 EXPOSE 8080

View File

@ -5,6 +5,8 @@ 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: build-app:
cd app && npm i && npm run build cd app && npm i && npm run build
build-dashboard:
cd dashboard && npm i && npm run build
clean: clean:
rm -rf build rm -rf build
test: test:

View File

@ -1,3 +1,14 @@
# Authorizer APP # Authorizer APP
App that can be used as login wall for your any application in combination with @authorizerdev/@authorizer.js App that can be used as login wall for your any application in combination with @authorizerdev/@authorizer.js
### Getting started
**Setting up locally**
- `cd app`
- `npm start`
**Creating production build**
- `make build-app`

12
dashboard/README.md Normal file
View File

@ -0,0 +1,12 @@
# Authorizer dashboard
### Getting started
**Setting up locally**
- `cd dashboard`
- `npm start`
**Creating production build**
- `make build-dashboard`

View File

@ -0,0 +1,12 @@
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__,
logLevel: 'info',
});

1682
dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
dashboard/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "dashboard",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js",
"start": "NODE_ENV=development node ./esbuild.config.js"
},
"keywords": [],
"author": "Lakhan Samani",
"license": "ISC",
"dependencies": {
"@chakra-ui/react": "^1.7.3",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.2",
"esbuild": "^0.14.9",
"framer-motion": "^5.5.5",
"graphql": "^16.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-icons": "^4.3.1",
"react-router-dom": "^6.2.1",
"typescript": "^4.5.4",
"urql": "^2.0.6"
}
}

44
dashboard/src/App.tsx Normal file
View File

@ -0,0 +1,44 @@
import * as React from "react";
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
import { BrowserRouter } from "react-router-dom";
import { createClient, Provider } from "urql";
import {AppRoutes} from './routes'
import { AuthContainer } from "./containers/AuthContainer";
const queryClient = createClient({
url: "/graphql",
fetchOptions: () => {
return {
credentials: "include",
};
},
});
const theme = extendTheme({
styles: {
global: {
"html, body, #root": {
height: "100%",
},
},
},
colors: {
blue: {
500: "rgb(59,130,246)",
},
},
});
export default function App() {
return (
<ChakraProvider theme={theme}>
<Provider value={queryClient}>
<BrowserRouter basename="/dashboard">
<AuthContainer>
<AppRoutes />
</AuthContainer>
</BrowserRouter>
</Provider>
</ChakraProvider>
);
}

View File

@ -0,0 +1,54 @@
import { Box, Image, Link, Text } from "@chakra-ui/react";
import { NavLink, useLocation } from "react-router-dom";
import React from "react";
import { LOGO_URL } from "../constants";
const routes = [
{
route: "/users",
name: "Users",
},
{
route: "/settings",
name: "Settings",
},
];
export const Sidebar = () => {
const { pathname } = useLocation();
return (
<Box as="nav" h="100%">
<NavLink to="/">
<Box d="flex" alignItems="center" p="4" mt="4" mb="10">
<Image w="8" src={LOGO_URL} alt="" />
<Text
color="white"
casing="uppercase"
fontSize="1xl"
ml="3"
letterSpacing="1.5px"
>
Authorizer
</Text>
</Box>
</NavLink>
{routes.map(({ route, name }: any) => (
<Link
color={pathname === route ? "blue.500" : "white"}
transition="all ease-in 0.2s"
_hover={{ color: pathname === route ? "blue.200" : "whiteAlpha.700" }}
px="4"
py="2"
bg={pathname === route ? "white" : ""}
fontWeight="bold"
display="block"
as={NavLink}
key={name}
to={route}
>
{name}
</Link>
))}
</Box>
);
};

View File

@ -0,0 +1 @@
export const LOGO_URL = "https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png"

View File

@ -0,0 +1,37 @@
import { Center, Spinner } from "@chakra-ui/react";
import React from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { useQuery } from "urql";
import { AdminSessionQuery } from "../graphql/queries";
import { hasAdminSecret } from "../utils";
export const AuthContainer = ({ children }: { children: any }) => {
const { pathname } = useLocation();
const isOnboardingComplete = hasAdminSecret();
const [result] = useQuery({
query: AdminSessionQuery,
pause: !isOnboardingComplete,
});
if (result.fetching) {
return (
<Center>
<Spinner />
</Center>
);
}
if (
result?.error?.message.includes("unauthorized") &&
pathname !== "/login"
) {
return <Navigate to="login" />;
}
if (!isOnboardingComplete && pathname !== "/setup") {
return <Navigate to="setup" />;
}
return children;
};

View File

@ -0,0 +1,15 @@
export const AdminSignup = `
mutation adminSignup($secret: String!) {
_admin_signup (params: {admin_secret: $secret}) {
message
}
}
`;
export const AdminLogin = `
mutation adminLogin($secret: String!){
_admin_login(params: { admin_secret: $secret }) {
message
}
}
`

View File

@ -0,0 +1,7 @@
export const AdminSessionQuery = `
query {
_admin_session{
message
}
}
`;

5
dashboard/src/index.tsx Normal file
View File

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

View File

@ -0,0 +1,29 @@
import { Box, Center, Flex, Image, Text } from "@chakra-ui/react";
import React from "react";
import { LOGO_URL } from "../constants";
export function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<Flex flexWrap="wrap" h="100%">
<Center h="100%" flex="3" bg="blue.500" flexDirection="column">
<Image
src={LOGO_URL}
alt=""
/>
<Text
color="white"
casing="uppercase"
fontSize="3xl"
mt="2"
letterSpacing="2.25px"
>
Authorizer
</Text>
</Center>
<Center h="100%" flex="2">
{children}
</Center>
</Flex>
);
}

View File

@ -0,0 +1,14 @@
import { Box, Flex } from "@chakra-ui/react";
import React from "react";
import { Sidebar } from "../components/Sidebar";
export function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<Flex flexWrap="wrap" h="100%">
<Box maxW="72" bg="blue.500" flex="1">
<Sidebar />
</Box>
<Box as="main" flex="2" p="10">{children}</Box>
</Flex>
);
}

View File

@ -0,0 +1,90 @@
import {
Button,
FormControl,
FormLabel,
Input,
useToast,
VStack,
} from "@chakra-ui/react";
import React, { useEffect } from "react";
import { useMutation } from "urql";
import { AuthLayout } from "../layouts/AuthLayout";
import { AdminLogin, AdminSignup } from "../graphql/mutation";
import { useLocation, useNavigate } from "react-router-dom";
export const Auth = () => {
const [loginResult, login] = useMutation(AdminLogin);
const [signUpResult, signup] = useMutation(AdminSignup);
const toast = useToast();
const navigate = useNavigate()
const { pathname } = useLocation();
const isLogin = pathname === "/login";
const handleAdminSecret = (e: any) => {
e.preventDefault();
const formValues = [...e.target.elements].reduce((agg: any, elem: any) => {
if (elem.id) {
return {
...agg,
[elem.id]: elem.value,
};
}
return agg;
}, {});
(isLogin ? login : signup)({
secret: formValues["admin-secret"],
}).then((res) => {
if (!res.error?.name) {
navigate("/");
}
});
};
const errors = isLogin ? loginResult.error : signUpResult.error;
useEffect(() => {
if (errors?.graphQLErrors) {
(errors?.graphQLErrors || []).map((error: any) => {
toast({
title: error.message,
isClosable: true,
status: "error",
position:"bottom-right"
});
})
}
}, [errors])
return (
<AuthLayout>
<form onSubmit={handleAdminSecret}>
<VStack spacing="2.5" justify="space-between">
<FormControl isRequired>
<FormLabel htmlFor="admin-secret">
{isLogin ? "Enter" : "Setup"} Admin Secret
</FormLabel>
<Input
size="lg"
id="admin-secret"
placeholder="Admin secret"
minLength={6}
/>
</FormControl>
<Button
isLoading={signUpResult.fetching || loginResult.fetching}
colorScheme="blue"
size="lg"
w="100%"
d="block"
type="submit"
>
{isLogin ? "Login" : "Sign up"}
</Button>
</VStack>
</form>
</AuthLayout>
);
};

View File

@ -0,0 +1,6 @@
import { Box } from "@chakra-ui/react";
import React from "react";
export function Home() {
return <Box>Welcome to Authorizer dashboard!</Box>;
}

View File

@ -0,0 +1,6 @@
import { Box } from "@chakra-ui/react";
import React from "react";
export function Users() {
return <Box>users</Box>;
}

View File

@ -0,0 +1,26 @@
import React from "react";
import { Outlet, Route, Routes } from "react-router-dom";
import { DashboardLayout } from "../layouts/DashboardLayout";
import { Auth } from "../pages/Auth";
import { Home } from "../pages/Home";
import { Users } from "../pages/Users";
export const AppRoutes = () => {
return (
<Routes>
<Route path="login" element={<Auth />} />
<Route path="setup" element={<Auth />} />
<Route
element={
<DashboardLayout>
<Outlet />
</DashboardLayout>
}
>
<Route path="/" element={<Home />} />
<Route path="users" element={<Users />} />
</Route>
</Routes>
);
};

View File

@ -0,0 +1,3 @@
export const hasAdminSecret = () => {
return (<any>window)["__authorizer__"].isOnboardingCompleted === true
}

72
dashboard/tsconfig.json Normal file
View File

@ -0,0 +1,72 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": ["es2018", "dom"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */,
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
"rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

View File

@ -1,7 +1,7 @@
VERSION="$1" VERSION="$1"
make clean && make build-app && 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 dashboard/build
AUTH="Authorization: token $GITHUB_TOKEN" AUTH="Authorization: token $GITHUB_TOKEN"
RELASE_INFO=$(curl -sH "$AUTH" https://api.github.com/repos/authorizerdev/authorizer/releases/tags/${VERSION}) RELASE_INFO=$(curl -sH "$AUTH" https://api.github.com/repos/authorizerdev/authorizer/releases/tags/${VERSION})
echo $RELASE_INFO echo $RELASE_INFO

View File

@ -0,0 +1,27 @@
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 adminLoginTests(s TestSetup, t *testing.T) {
t.Run(`should complete admin login`, func(t *testing.T) {
_, ctx := createContext(s)
_, err := resolvers.AdminLoginResolver(ctx, model.AdminLoginInput{
AdminSecret: "admin_test",
})
assert.NotNil(t, err)
_, err = resolvers.AdminLoginResolver(ctx, model.AdminLoginInput{
AdminSecret: constants.EnvData.ADMIN_SECRET,
})
assert.Nil(t, err)
})
}

View File

@ -0,0 +1,26 @@
package test
import (
"fmt"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert"
)
func adminLogoutTests(s TestSetup, t *testing.T) {
t.Run(`should get admin session`, func(t *testing.T) {
req, ctx := createContext(s)
_, err := resolvers.AdminLogout(ctx)
assert.NotNil(t, err)
h, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
_, err = resolvers.AdminLogout(ctx)
assert.Nil(t, err)
})
}

View File

@ -0,0 +1,28 @@
package test
import (
"fmt"
"log"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert"
)
func adminSessionTests(s TestSetup, t *testing.T) {
t.Run(`should get admin session`, func(t *testing.T) {
req, ctx := createContext(s)
_, err := resolvers.AdminSession(ctx)
log.Println("error:", err)
assert.NotNil(t, err)
h, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
_, err = resolvers.AdminSession(ctx)
assert.Nil(t, err)
})
}

View File

@ -0,0 +1,31 @@
package test
import (
"log"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func adminSignupTests(s TestSetup, t *testing.T) {
t.Run(`should complete admin login`, func(t *testing.T) {
_, ctx := createContext(s)
_, err := resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{
AdminSecret: "admin",
})
log.Println("err", err)
assert.NotNil(t, err)
// reset env for test to pass
constants.EnvData.ADMIN_SECRET = ""
_, err = resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{
AdminSecret: uuid.New().String(),
})
assert.Nil(t, err)
})
}

View File

@ -0,0 +1,29 @@
package test
import (
"fmt"
"log"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert"
)
func configTests(s TestSetup, t *testing.T) {
t.Run(`should get config`, func(t *testing.T) {
req, ctx := createContext(s)
_, err := resolvers.ConfigResolver(ctx)
log.Println("error:", err)
assert.NotNil(t, err)
h, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
res, err := resolvers.ConfigResolver(ctx)
assert.Nil(t, err)
assert.Equal(t, *res.AdminSecret, constants.EnvData.ADMIN_SECRET)
})
}

View File

@ -1,11 +1,13 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers" "github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -24,7 +26,10 @@ func deleteUserTest(s TestSetup, t *testing.T) {
}) })
assert.NotNil(t, err, "unauthorized") assert.NotNil(t, err, "unauthorized")
req.Header.Add("x-authorizer-admin-secret", constants.ADMIN_SECRET) h, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
_, err = resolvers.DeleteUser(ctx, model.DeleteUserInput{ _, err = resolvers.DeleteUser(ctx, model.DeleteUserInput{
Email: email, Email: email,
}) })

View File

@ -8,18 +8,18 @@ import (
) )
func TestEnvs(t *testing.T) { func TestEnvs(t *testing.T) {
constants.ENV_PATH = "../../.env.sample" constants.EnvData.ENV_PATH = "../../.env.sample"
assert.Equal(t, constants.ADMIN_SECRET, "admin") assert.Equal(t, constants.EnvData.ADMIN_SECRET, "admin")
assert.Equal(t, constants.ENV, "production") assert.Equal(t, constants.EnvData.ENV, "production")
assert.False(t, constants.DISABLE_EMAIL_VERIFICATION) assert.False(t, constants.EnvData.DISABLE_EMAIL_VERIFICATION)
assert.False(t, constants.DISABLE_MAGIC_LINK_LOGIN) assert.False(t, constants.EnvData.DISABLE_MAGIC_LINK_LOGIN)
assert.False(t, constants.DISABLE_BASIC_AUTHENTICATION) assert.False(t, constants.EnvData.DISABLE_BASIC_AUTHENTICATION)
assert.Equal(t, constants.JWT_TYPE, "HS256") assert.Equal(t, constants.EnvData.JWT_TYPE, "HS256")
assert.Equal(t, constants.JWT_SECRET, "random_string") assert.Equal(t, constants.EnvData.JWT_SECRET, "random_string")
assert.Equal(t, constants.JWT_ROLE_CLAIM, "role") assert.Equal(t, constants.EnvData.JWT_ROLE_CLAIM, "role")
assert.EqualValues(t, constants.ROLES, []string{"user"}) assert.EqualValues(t, constants.EnvData.ROLES, []string{"user"})
assert.EqualValues(t, constants.DEFAULT_ROLES, []string{"user"}) assert.EqualValues(t, constants.EnvData.DEFAULT_ROLES, []string{"user"})
assert.EqualValues(t, constants.PROTECTED_ROLES, []string{"admin"}) assert.EqualValues(t, constants.EnvData.PROTECTED_ROLES, []string{"admin"})
assert.EqualValues(t, constants.ALLOWED_ORIGINS, []string{"*"}) assert.EqualValues(t, constants.EnvData.ALLOWED_ORIGINS, []string{"*"})
} }

View File

@ -1,8 +1,10 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@ -25,7 +27,7 @@ func logoutTests(s TestSetup, t *testing.T) {
}) })
token := *verifyRes.AccessToken token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.COOKIE_NAME, token))
_, err = resolvers.Logout(ctx) _, err = resolvers.Logout(ctx)
assert.Nil(t, err) assert.Nil(t, err)
_, err = resolvers.Profile(ctx) _, err = resolvers.Profile(ctx)

View File

@ -1,8 +1,10 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@ -26,7 +28,7 @@ func magicLinkLoginTests(s TestSetup, t *testing.T) {
}) })
token := *verifyRes.AccessToken token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.COOKIE_NAME, token))
_, err = resolvers.Profile(ctx) _, err = resolvers.Profile(ctx)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -2,6 +2,7 @@ package test
import ( import (
"context" "context"
"log"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/resolvers" "github.com/authorizerdev/authorizer/server/resolvers"
@ -12,6 +13,7 @@ func metaTests(s TestSetup, t *testing.T) {
t.Run(`should get meta information`, func(t *testing.T) { t.Run(`should get meta information`, func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
meta, err := resolvers.Meta(ctx) meta, err := resolvers.Meta(ctx)
log.Println("=> meta:", meta)
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, meta.IsFacebookLoginEnabled) assert.False(t, meta.IsFacebookLoginEnabled)
assert.False(t, meta.IsGoogleLoginEnabled) assert.False(t, meta.IsGoogleLoginEnabled)

View File

@ -1,8 +1,10 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@ -30,7 +32,7 @@ func profileTests(s TestSetup, t *testing.T) {
}) })
token := *verifyRes.AccessToken token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.COOKIE_NAME, token))
profileRes, err := resolvers.Profile(ctx) profileRes, err := resolvers.Profile(ctx)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -1,11 +1,13 @@
package test package test
import ( import (
"log"
"testing" "testing"
"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/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/env"
) )
func TestResolvers(t *testing.T) { func TestResolvers(t *testing.T) {
@ -16,14 +18,36 @@ func TestResolvers(t *testing.T) {
} }
for dbType, dbURL := range databases { for dbType, dbURL := range databases {
constants.DATABASE_URL = dbURL constants.EnvData.DATABASE_URL = dbURL
constants.DATABASE_TYPE = dbType constants.EnvData.DATABASE_TYPE = dbType
db.InitDB() db.InitDB()
// clean the persisted config for test to use fresh config
config, err := db.Mgr.GetConfig()
if err == nil {
config.Config = []byte{}
db.Mgr.UpdateConfig(config)
}
env.PersistEnv()
s := testSetup() s := testSetup()
defer s.Server.Close() defer s.Server.Close()
log.Println("EnvData:", constants.EnvData)
t.Run("should pass tests for "+dbType, func(t *testing.T) { t.Run("should pass tests for "+dbType, func(t *testing.T) {
// admin tests
adminSignupTests(s, t)
verificationRequestsTest(s, t)
usersTest(s, t)
deleteUserTest(s, t)
updateUserTest(s, t)
adminLoginTests(s, t)
adminLogoutTests(s, t)
adminSessionTests(s, t)
updateConfigTests(s, t)
configTests(s, t)
// user tests
loginTests(s, t) loginTests(s, t)
signupTests(s, t) signupTests(s, t)
forgotPasswordTest(s, t) forgotPasswordTest(s, t)
@ -36,12 +60,6 @@ func TestResolvers(t *testing.T) {
magicLinkLoginTests(s, t) magicLinkLoginTests(s, t)
logoutTests(s, t) logoutTests(s, t)
metaTests(s, t) metaTests(s, t)
// admin tests
verificationRequestsTest(s, t)
usersTest(s, t)
deleteUserTest(s, t)
updateUserTest(s, t)
}) })
} }
} }

View File

@ -1,8 +1,10 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@ -30,7 +32,8 @@ func sessionTests(s TestSetup, t *testing.T) {
}) })
token := *verifyRes.AccessToken token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.COOKIE_NAME, token))
sessionRes, err := resolvers.Session(ctx, []string{}) sessionRes, err := resolvers.Session(ctx, []string{})
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -72,7 +72,8 @@ func testSetup() TestSetup {
Password: "test", Password: "test",
} }
constants.ENV_PATH = "../../.env.sample" constants.EnvData.ENV_PATH = "../../.env.sample"
env.InitEnv() env.InitEnv()
session.InitSession() session.InitSession()

View File

@ -0,0 +1,44 @@
package test
import (
"fmt"
"log"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert"
)
func updateConfigTests(s TestSetup, t *testing.T) {
t.Run(`should update configs`, func(t *testing.T) {
req, ctx := createContext(s)
originalAppURL := constants.EnvData.APP_URL
log.Println("=> originalAppURL:", constants.EnvData.APP_URL)
data := model.UpdateConfigInput{}
_, err := resolvers.UpdateConfigResolver(ctx, data)
log.Println("error:", err)
assert.NotNil(t, err)
h, _ := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
newURL := "https://test.com"
data = model.UpdateConfigInput{
AppURL: &newURL,
}
_, err = resolvers.UpdateConfigResolver(ctx, data)
log.Println("error:", err)
assert.Nil(t, err)
assert.Equal(t, constants.EnvData.APP_URL, newURL)
assert.NotEqual(t, constants.EnvData.APP_URL, originalAppURL)
data = model.UpdateConfigInput{
AppURL: &originalAppURL,
}
_, err = resolvers.UpdateConfigResolver(ctx, data)
assert.Nil(t, err)
})
}

View File

@ -1,8 +1,10 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@ -33,7 +35,7 @@ func updateProfileTests(s TestSetup, t *testing.T) {
}) })
token := *verifyRes.AccessToken token := *verifyRes.AccessToken
req.Header.Add("Authorization", "Bearer "+token) req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.COOKIE_NAME, token))
_, err = resolvers.UpdateProfile(ctx, model.UpdateProfileInput{ _, err = resolvers.UpdateProfile(ctx, model.UpdateProfileInput{
FamilyName: &fName, FamilyName: &fName,
}) })

View File

@ -1,11 +1,13 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers" "github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -29,7 +31,9 @@ func updateUserTest(s TestSetup, t *testing.T) {
}) })
assert.NotNil(t, err, "unauthorized") assert.NotNil(t, err, "unauthorized")
req.Header.Add("x-authorizer-admin-secret", constants.ADMIN_SECRET) h, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
_, err = resolvers.UpdateUser(ctx, model.UpdateUserInput{ _, err = resolvers.UpdateUser(ctx, model.UpdateUserInput{
ID: user.ID, ID: user.ID,
Roles: newRoles, Roles: newRoles,

View File

@ -1,11 +1,13 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers" "github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -22,7 +24,9 @@ func usersTest(s TestSetup, t *testing.T) {
users, err := resolvers.Users(ctx) users, err := resolvers.Users(ctx)
assert.NotNil(t, err, "unauthorized") assert.NotNil(t, err, "unauthorized")
req.Header.Add("x-authorizer-admin-secret", constants.ADMIN_SECRET) h, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
users, err = resolvers.Users(ctx) users, err = resolvers.Users(ctx)
assert.Nil(t, err) assert.Nil(t, err)
rLen := len(users) rLen := len(users)

View File

@ -22,7 +22,7 @@ func TestIsValidEmail(t *testing.T) {
func TestIsValidOrigin(t *testing.T) { func TestIsValidOrigin(t *testing.T) {
// don't use portocal(http/https) for ALLOWED_ORIGINS while testing, // don't use portocal(http/https) for ALLOWED_ORIGINS while testing,
// as we trim them off while running the main function // as we trim them off while running the main function
constants.ALLOWED_ORIGINS = []string{"localhost:8080", "*.google.com", "*.google.in", "*abc.*"} constants.EnvData.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://myapp.com"), "it should be invalid origin")
assert.False(t, utils.IsValidOrigin("http://appgoogle.com"), "it should be invalid origin") assert.False(t, utils.IsValidOrigin("http://appgoogle.com"), "it should be invalid origin")

View File

@ -1,11 +1,13 @@
package test package test
import ( import (
"fmt"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers" "github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -23,7 +25,9 @@ func verificationRequestsTest(s TestSetup, t *testing.T) {
requests, err := resolvers.VerificationRequests(ctx) requests, err := resolvers.VerificationRequests(ctx)
assert.NotNil(t, err, "unauthorizer") assert.NotNil(t, err, "unauthorizer")
req.Header.Add("x-authorizer-admin-secret", constants.ADMIN_SECRET) h, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
assert.Nil(t, err)
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", constants.EnvData.ADMIN_COOKIE_NAME, h))
requests, err = resolvers.VerificationRequests(ctx) requests, err = resolvers.VerificationRequests(ctx)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -1,51 +1,62 @@
package constants package constants
// this constants are configured via env type EnvConst struct {
var ( ADMIN_SECRET string
ADMIN_SECRET = "" ENV string
ENV = "" ENV_PATH string
ENV_PATH = "" VERSION string
VERSION = "" DATABASE_TYPE string
DATABASE_TYPE = "" DATABASE_URL string
DATABASE_URL = "" DATABASE_NAME string
DATABASE_NAME = "" SMTP_HOST string
SMTP_HOST = "" SMTP_PORT string
SMTP_PORT = "" SMTP_PASSWORD string
SMTP_USERNAME = "" SMTP_USERNAME string
SMTP_PASSWORD = "" SENDER_EMAIL string
SENDER_EMAIL = "" JWT_TYPE string
JWT_TYPE = "" JWT_SECRET string
JWT_SECRET = "" ALLOWED_ORIGINS []string
ALLOWED_ORIGINS = []string{} AUTHORIZER_URL string
AUTHORIZER_URL = "" APP_URL string
APP_URL = "" PORT string
PORT = "" REDIS_URL string
REDIS_URL = "" COOKIE_NAME string
IS_PROD = false ADMIN_COOKIE_NAME string
COOKIE_NAME = "" RESET_PASSWORD_URL string
RESET_PASSWORD_URL = "" ENCRYPTION_KEY string `json:"-"`
DISABLE_EMAIL_VERIFICATION = false IS_PROD bool
DISABLE_BASIC_AUTHENTICATION = false DISABLE_EMAIL_VERIFICATION bool
DISABLE_MAGIC_LINK_LOGIN = false DISABLE_BASIC_AUTHENTICATION bool
DISABLE_LOGIN_PAGE = false DISABLE_MAGIC_LINK_LOGIN bool
DISABLE_LOGIN_PAGE bool
// ROLES // ROLES
ROLES = []string{} ROLES []string
PROTECTED_ROLES = []string{} PROTECTED_ROLES []string
DEFAULT_ROLES = []string{} DEFAULT_ROLES []string
JWT_ROLE_CLAIM = "role" JWT_ROLE_CLAIM string
// OAuth login // OAuth login
GOOGLE_CLIENT_ID = "" GOOGLE_CLIENT_ID string
GOOGLE_CLIENT_SECRET = "" GOOGLE_CLIENT_SECRET string
GITHUB_CLIENT_ID = "" GITHUB_CLIENT_ID string
GITHUB_CLIENT_SECRET = "" GITHUB_CLIENT_SECRET string
FACEBOOK_CLIENT_ID = "" FACEBOOK_CLIENT_ID string
FACEBOOK_CLIENT_SECRET = "" FACEBOOK_CLIENT_SECRET string
TWITTER_CLIENT_ID = ""
TWITTER_CLIENT_SECRET = ""
// Org envs // Org envs
ORGANIZATION_NAME = "Authorizer" ORGANIZATION_NAME string
ORGANIZATION_LOGO = "https://authorizer.dev/images/logo.png" ORGANIZATION_LOGO string
) }
var EnvData = EnvConst{
ADMIN_COOKIE_NAME: "authorizer-admin",
JWT_ROLE_CLAIM: "role",
ORGANIZATION_NAME: "Authorizer",
ORGANIZATION_LOGO: "https://authorizer.dev/images/logo.png",
DISABLE_EMAIL_VERIFICATION: false,
DISABLE_BASIC_AUTHENTICATION: false,
DISABLE_MAGIC_LINK_LOGIN: false,
DISABLE_LOGIN_PAGE: false,
IS_PROD: false,
}

View File

@ -17,7 +17,7 @@ import (
func initArangodb() (arangoDriver.Database, error) { func initArangodb() (arangoDriver.Database, error) {
ctx := context.Background() ctx := context.Background()
conn, err := http.NewConnection(http.ConnectionConfig{ conn, err := http.NewConnection(http.ConnectionConfig{
Endpoints: []string{constants.DATABASE_URL}, Endpoints: []string{constants.EnvData.DATABASE_URL},
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -32,16 +32,16 @@ func initArangodb() (arangoDriver.Database, error) {
var arangodb driver.Database var arangodb driver.Database
arangodb_exists, err := arangoClient.DatabaseExists(nil, constants.DATABASE_NAME) arangodb_exists, err := arangoClient.DatabaseExists(nil, constants.EnvData.DATABASE_NAME)
if arangodb_exists { if arangodb_exists {
log.Println(constants.DATABASE_NAME + " db exists already") log.Println(constants.EnvData.DATABASE_NAME + " db exists already")
arangodb, err = arangoClient.Database(nil, constants.DATABASE_NAME) arangodb, err = arangoClient.Database(nil, constants.EnvData.DATABASE_NAME)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else { } else {
arangodb, err = arangoClient.CreateDatabase(nil, constants.DATABASE_NAME, nil) arangodb, err = arangoClient.CreateDatabase(nil, constants.EnvData.DATABASE_NAME, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -100,5 +100,15 @@ func initArangodb() (arangoDriver.Database, error) {
Sparse: true, Sparse: true,
}) })
configCollectionExists, err := arangodb.CollectionExists(ctx, Collections.Config)
if configCollectionExists {
log.Println(Collections.Config + " collection exists already")
} else {
_, err = arangodb.CreateCollection(ctx, Collections.Config, nil)
if err != nil {
log.Println("error creating collection("+Collections.Config+"):", err)
}
}
return arangodb, err return arangodb, err
} }

161
server/db/config.go Normal file
View File

@ -0,0 +1,161 @@
package db
import (
"fmt"
"log"
"time"
arangoDriver "github.com/arangodb/go-driver"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Config struct {
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
Config []byte `gorm:"type:text" json:"config" bson:"config"`
Hash string `gorm:"type:hash" json:"hash" bson:"hash"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
}
// AddConfig function to add config
func (mgr *manager) AddConfig(config Config) (Config, error) {
if config.ID == "" {
config.ID = uuid.New().String()
}
if IsORMSupported {
// copy id as value for fields required for mongodb & arangodb
config.Key = config.ID
result := mgr.sqlDB.Create(&config)
if result.Error != nil {
log.Println("error adding config:", result.Error)
return config, result.Error
}
}
if IsArangoDB {
config.CreatedAt = time.Now().Unix()
config.UpdatedAt = time.Now().Unix()
configCollection, _ := mgr.arangodb.Collection(nil, Collections.Config)
meta, err := configCollection.CreateDocument(arangoDriver.WithOverwrite(nil), config)
if err != nil {
log.Println("error adding config:", err)
return config, err
}
config.Key = meta.Key
config.ID = meta.ID.String()
}
if IsMongoDB {
config.CreatedAt = time.Now().Unix()
config.UpdatedAt = time.Now().Unix()
config.Key = config.ID
configCollection := mgr.mongodb.Collection(Collections.Config, options.Collection())
_, err := configCollection.InsertOne(nil, config)
if err != nil {
log.Println("error adding config:", err)
return config, err
}
}
return config, nil
}
// UpdateConfig function to update config
func (mgr *manager) UpdateConfig(config Config) (Config, error) {
config.UpdatedAt = time.Now().Unix()
if IsORMSupported {
result := mgr.sqlDB.Save(&config)
if result.Error != nil {
log.Println("error updating config:", result.Error)
return config, result.Error
}
}
if IsArangoDB {
collection, _ := mgr.arangodb.Collection(nil, Collections.Config)
meta, err := collection.UpdateDocument(nil, config.Key, config)
if err != nil {
log.Println("error updating config:", err)
return config, err
}
config.Key = meta.Key
config.ID = meta.ID.String()
}
if IsMongoDB {
configCollection := mgr.mongodb.Collection(Collections.Config, options.Collection())
_, err := configCollection.UpdateOne(nil, bson.M{"_id": bson.M{"$eq": config.ID}}, bson.M{"$set": config}, options.MergeUpdateOptions())
if err != nil {
log.Println("error updating config:", err)
return config, err
}
}
return config, nil
}
// GetConfig function to get config
func (mgr *manager) GetConfig() (Config, error) {
var config Config
if IsORMSupported {
result := mgr.sqlDB.First(&config)
if result.Error != nil {
return config, result.Error
}
}
if IsArangoDB {
query := fmt.Sprintf("FOR d in %s RETURN d", Collections.Config)
cursor, err := mgr.arangodb.Query(nil, query, nil)
if err != nil {
return config, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if config.Key == "" {
return config, fmt.Errorf("config not found")
}
break
}
_, err := cursor.ReadDocument(nil, &config)
if err != nil {
return config, err
}
}
}
if IsMongoDB {
configCollection := mgr.mongodb.Collection(Collections.Config, options.Collection())
cursor, err := configCollection.Find(nil, bson.M{}, options.Find())
if err != nil {
return config, err
}
defer cursor.Close(nil)
for cursor.Next(nil) {
err := cursor.Decode(&config)
if err != nil {
return config, err
}
}
if config.ID == "" {
return config, fmt.Errorf("config not found")
}
}
return config, nil
}

View File

@ -29,6 +29,9 @@ type Manager interface {
GetVerificationByEmail(email string, identifier string) (VerificationRequest, error) GetVerificationByEmail(email string, identifier string) (VerificationRequest, error)
AddSession(session Session) error AddSession(session Session) error
DeleteUserSession(userId string) error DeleteUserSession(userId string) error
AddConfig(config Config) (Config, error)
UpdateConfig(config Config) (Config, error)
GetConfig() (Config, error)
} }
type manager struct { type manager struct {
@ -42,6 +45,7 @@ type CollectionList struct {
User string User string
VerificationRequest string VerificationRequest string
Session string Session string
Config string
} }
var ( var (
@ -54,6 +58,7 @@ var (
User: Prefix + "users", User: Prefix + "users",
VerificationRequest: Prefix + "verification_requests", VerificationRequest: Prefix + "verification_requests",
Session: Prefix + "sessions", Session: Prefix + "sessions",
Config: Prefix + "config",
} }
) )
@ -61,9 +66,9 @@ func InitDB() {
var sqlDB *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() IsORMSupported = constants.EnvData.DATABASE_TYPE != enum.Arangodb.String() && constants.EnvData.DATABASE_TYPE != enum.Mongodb.String()
IsArangoDB = constants.DATABASE_TYPE == enum.Arangodb.String() IsArangoDB = constants.EnvData.DATABASE_TYPE == enum.Arangodb.String()
IsMongoDB = constants.DATABASE_TYPE == enum.Mongodb.String() IsMongoDB = constants.EnvData.DATABASE_TYPE == enum.Mongodb.String()
// sql db orm config // sql db orm config
ormConfig := &gorm.Config{ ormConfig := &gorm.Config{
@ -72,20 +77,20 @@ func InitDB() {
}, },
} }
log.Println("db type:", constants.DATABASE_TYPE) log.Println("db type:", constants.EnvData.DATABASE_TYPE)
switch constants.DATABASE_TYPE { switch constants.EnvData.DATABASE_TYPE {
case enum.Postgres.String(): case enum.Postgres.String():
sqlDB, err = gorm.Open(postgres.Open(constants.DATABASE_URL), ormConfig) sqlDB, err = gorm.Open(postgres.Open(constants.EnvData.DATABASE_URL), ormConfig)
break break
case enum.Sqlite.String(): case enum.Sqlite.String():
sqlDB, err = gorm.Open(sqlite.Open(constants.DATABASE_URL), ormConfig) sqlDB, err = gorm.Open(sqlite.Open(constants.EnvData.DATABASE_URL), ormConfig)
break break
case enum.Mysql.String(): case enum.Mysql.String():
sqlDB, err = gorm.Open(mysql.Open(constants.DATABASE_URL), ormConfig) sqlDB, err = gorm.Open(mysql.Open(constants.EnvData.DATABASE_URL), ormConfig)
break break
case enum.SQLServer.String(): case enum.SQLServer.String():
sqlDB, err = gorm.Open(sqlserver.Open(constants.DATABASE_URL), ormConfig) sqlDB, err = gorm.Open(sqlserver.Open(constants.EnvData.DATABASE_URL), ormConfig)
break break
case enum.Arangodb.String(): case enum.Arangodb.String():
arangodb, err := initArangodb() arangodb, err := initArangodb()
@ -118,7 +123,7 @@ func InitDB() {
if err != nil { if err != nil {
log.Fatal("Failed to init sqlDB:", err) log.Fatal("Failed to init sqlDB:", err)
} else { } else {
sqlDB.AutoMigrate(&User{}, &VerificationRequest{}, &Session{}) sqlDB.AutoMigrate(&User{}, &VerificationRequest{}, &Session{}, &Config{})
} }
Mgr = &manager{ Mgr = &manager{
sqlDB: sqlDB, sqlDB: sqlDB,

View File

@ -12,7 +12,7 @@ import (
) )
func initMongodb() (*mongo.Database, error) { func initMongodb() (*mongo.Database, error) {
mongodbOptions := options.Client().ApplyURI(constants.DATABASE_URL) mongodbOptions := options.Client().ApplyURI(constants.EnvData.DATABASE_URL)
maxWait := time.Duration(5 * time.Second) maxWait := time.Duration(5 * time.Second)
mongodbOptions.ConnectTimeout = &maxWait mongodbOptions.ConnectTimeout = &maxWait
mongoClient, err := mongo.NewClient(mongodbOptions) mongoClient, err := mongo.NewClient(mongodbOptions)
@ -30,7 +30,7 @@ func initMongodb() (*mongo.Database, error) {
return nil, err return nil, err
} }
mongodb := mongoClient.Database(constants.DATABASE_NAME, options.Database()) mongodb := mongoClient.Database(constants.EnvData.DATABASE_NAME, options.Database())
mongodb.CreateCollection(ctx, Collections.User, options.CreateCollection()) mongodb.CreateCollection(ctx, Collections.User, options.CreateCollection())
userCollection := mongodb.Collection(Collections.User, options.Collection()) userCollection := mongodb.Collection(Collections.User, options.Collection())
@ -73,5 +73,7 @@ func initMongodb() (*mongo.Database, error) {
}, },
}, options.CreateIndexes()) }, options.CreateIndexes())
mongodb.CreateCollection(ctx, Collections.Config, options.CreateCollection())
return mongodb, nil return mongodb, nil
} }

View File

@ -43,7 +43,7 @@ func (mgr *manager) AddUser(user User) (User, error) {
} }
if user.Roles == "" { if user.Roles == "" {
user.Roles = constants.DEFAULT_ROLES[0] user.Roles = constants.EnvData.DEFAULT_ROLES[0]
} }
if IsORMSupported { if IsORMSupported {

View File

@ -11,13 +11,13 @@ import (
func SendMail(to []string, Subject, bodyMessage string) error { func SendMail(to []string, Subject, bodyMessage string) error {
m := gomail.NewMessage() m := gomail.NewMessage()
m.SetHeader("From", constants.SENDER_EMAIL) m.SetHeader("From", constants.EnvData.SENDER_EMAIL)
m.SetHeader("To", to...) m.SetHeader("To", to...)
m.SetHeader("Subject", Subject) m.SetHeader("Subject", Subject)
m.SetBody("text/html", bodyMessage) m.SetBody("text/html", bodyMessage)
port, _ := strconv.Atoi(constants.SMTP_PORT) port, _ := strconv.Atoi(constants.EnvData.SMTP_PORT)
d := gomail.NewDialer(constants.SMTP_HOST, port, constants.SMTP_USERNAME, constants.SMTP_PASSWORD) d := gomail.NewDialer(constants.EnvData.SMTP_HOST, port, constants.EnvData.SMTP_USERNAME, constants.EnvData.SMTP_PASSWORD)
if constants.ENV == "development" { if constants.EnvData.ENV == "development" {
d.TLSConfig = &tls.Config{InsecureSkipVerify: true} d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
} }
if err := d.DialAndSend(m); err != nil { if err := d.DialAndSend(m); err != nil {

235
server/env/env.go vendored
View File

@ -7,6 +7,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/google/uuid"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
@ -20,167 +21,173 @@ var (
// InitEnv -> to initialize env and through error if required env are not present // InitEnv -> to initialize env and through error if required env are not present
func InitEnv() { func InitEnv() {
if constants.ENV_PATH == "" { if constants.EnvData.ENV_PATH == "" {
constants.ENV_PATH = `.env` constants.EnvData.ENV_PATH = `.env`
} }
if ARG_ENV_FILE != nil && *ARG_ENV_FILE != "" { if ARG_ENV_FILE != nil && *ARG_ENV_FILE != "" {
constants.ENV_PATH = *ARG_ENV_FILE constants.EnvData.ENV_PATH = *ARG_ENV_FILE
} }
err := godotenv.Load(constants.ENV_PATH) err := godotenv.Load(constants.EnvData.ENV_PATH)
if err != nil { if err != nil {
log.Printf("error loading %s file", constants.ENV_PATH) log.Printf("error loading %s file", constants.EnvData.ENV_PATH)
} }
if constants.ADMIN_SECRET == "" { if constants.EnvData.ADMIN_SECRET == "" {
constants.ADMIN_SECRET = os.Getenv("ADMIN_SECRET") constants.EnvData.ADMIN_SECRET = os.Getenv("ADMIN_SECRET")
if constants.ADMIN_SECRET == "" {
panic("root admin secret is required")
}
} }
if constants.ENV == "" { if constants.EnvData.DATABASE_TYPE == "" {
constants.ENV = os.Getenv("ENV") constants.EnvData.DATABASE_TYPE = os.Getenv("DATABASE_TYPE")
if constants.ENV == "" { log.Println(constants.EnvData.DATABASE_TYPE)
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 != "" { if ARG_DB_TYPE != nil && *ARG_DB_TYPE != "" {
constants.DATABASE_TYPE = *ARG_DB_TYPE constants.EnvData.DATABASE_TYPE = *ARG_DB_TYPE
} }
if constants.DATABASE_TYPE == "" { if constants.EnvData.DATABASE_TYPE == "" {
panic("DATABASE_TYPE is required") panic("DATABASE_TYPE is required")
} }
} }
if constants.DATABASE_URL == "" { if constants.EnvData.DATABASE_URL == "" {
constants.DATABASE_URL = os.Getenv("DATABASE_URL") constants.EnvData.DATABASE_URL = os.Getenv("DATABASE_URL")
if ARG_DB_URL != nil && *ARG_DB_URL != "" { if ARG_DB_URL != nil && *ARG_DB_URL != "" {
constants.DATABASE_URL = *ARG_DB_URL constants.EnvData.DATABASE_URL = *ARG_DB_URL
} }
if constants.DATABASE_URL == "" { if constants.EnvData.DATABASE_URL == "" {
panic("DATABASE_URL is required") panic("DATABASE_URL is required")
} }
} }
if constants.DATABASE_NAME == "" { if constants.EnvData.DATABASE_NAME == "" {
constants.DATABASE_NAME = os.Getenv("DATABASE_NAME") constants.EnvData.DATABASE_NAME = os.Getenv("DATABASE_NAME")
if constants.DATABASE_NAME == "" { if constants.EnvData.DATABASE_NAME == "" {
constants.DATABASE_NAME = "authorizer" constants.EnvData.DATABASE_NAME = "authorizer"
} }
} }
if constants.SMTP_HOST == "" { if constants.EnvData.ENV == "" {
constants.SMTP_HOST = os.Getenv("SMTP_HOST") constants.EnvData.ENV = os.Getenv("ENV")
} if constants.EnvData.ENV == "" {
constants.EnvData.ENV = "production"
}
if constants.SMTP_PORT == "" { if constants.EnvData.ENV == "production" {
constants.SMTP_PORT = os.Getenv("SMTP_PORT") constants.EnvData.IS_PROD = true
} os.Setenv("GIN_MODE", "release")
} else {
if constants.SMTP_USERNAME == "" { constants.EnvData.IS_PROD = false
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 == "" { if constants.EnvData.SMTP_HOST == "" {
constants.AUTHORIZER_URL = strings.TrimSuffix(os.Getenv("AUTHORIZER_URL"), "/") constants.EnvData.SMTP_HOST = os.Getenv("SMTP_HOST")
}
if constants.EnvData.SMTP_PORT == "" {
constants.EnvData.SMTP_PORT = os.Getenv("SMTP_PORT")
}
if constants.EnvData.SMTP_USERNAME == "" {
constants.EnvData.SMTP_USERNAME = os.Getenv("SMTP_USERNAME")
}
if constants.EnvData.SMTP_PASSWORD == "" {
constants.EnvData.SMTP_PASSWORD = os.Getenv("SMTP_PASSWORD")
}
if constants.EnvData.SENDER_EMAIL == "" {
constants.EnvData.SENDER_EMAIL = os.Getenv("SENDER_EMAIL")
}
if constants.EnvData.JWT_SECRET == "" {
constants.EnvData.JWT_SECRET = os.Getenv("JWT_SECRET")
if constants.EnvData.JWT_SECRET == "" {
constants.EnvData.JWT_SECRET = uuid.New().String()
}
}
if constants.EnvData.JWT_TYPE == "" {
constants.EnvData.JWT_TYPE = os.Getenv("JWT_TYPE")
if constants.EnvData.JWT_TYPE == "" {
constants.EnvData.JWT_TYPE = "HS256"
}
}
if constants.EnvData.JWT_ROLE_CLAIM == "" {
constants.EnvData.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM")
if constants.EnvData.JWT_ROLE_CLAIM == "" {
constants.EnvData.JWT_ROLE_CLAIM = "role"
}
}
if constants.EnvData.AUTHORIZER_URL == "" {
constants.EnvData.AUTHORIZER_URL = strings.TrimSuffix(os.Getenv("AUTHORIZER_URL"), "/")
if ARG_AUTHORIZER_URL != nil && *ARG_AUTHORIZER_URL != "" { if ARG_AUTHORIZER_URL != nil && *ARG_AUTHORIZER_URL != "" {
constants.AUTHORIZER_URL = *ARG_AUTHORIZER_URL constants.EnvData.AUTHORIZER_URL = *ARG_AUTHORIZER_URL
} }
} }
if constants.PORT == "" { if constants.EnvData.PORT == "" {
constants.PORT = os.Getenv("PORT") constants.EnvData.PORT = os.Getenv("PORT")
if constants.PORT == "" { if constants.EnvData.PORT == "" {
constants.PORT = "8080" constants.EnvData.PORT = "8080"
} }
} }
if constants.REDIS_URL == "" { if constants.EnvData.REDIS_URL == "" {
constants.REDIS_URL = os.Getenv("REDIS_URL") constants.EnvData.REDIS_URL = os.Getenv("REDIS_URL")
} }
if constants.COOKIE_NAME == "" { if constants.EnvData.COOKIE_NAME == "" {
constants.COOKIE_NAME = os.Getenv("COOKIE_NAME") constants.EnvData.COOKIE_NAME = os.Getenv("COOKIE_NAME")
if constants.EnvData.COOKIE_NAME == "" {
constants.EnvData.COOKIE_NAME = "authorizer"
}
} }
if constants.GOOGLE_CLIENT_ID == "" { if constants.EnvData.GOOGLE_CLIENT_ID == "" {
constants.GOOGLE_CLIENT_ID = os.Getenv("GOOGLE_CLIENT_ID") constants.EnvData.GOOGLE_CLIENT_ID = os.Getenv("GOOGLE_CLIENT_ID")
} }
if constants.GOOGLE_CLIENT_SECRET == "" { if constants.EnvData.GOOGLE_CLIENT_SECRET == "" {
constants.GOOGLE_CLIENT_SECRET = os.Getenv("GOOGLE_CLIENT_SECRET") constants.EnvData.GOOGLE_CLIENT_SECRET = os.Getenv("GOOGLE_CLIENT_SECRET")
} }
if constants.GITHUB_CLIENT_ID == "" { if constants.EnvData.GITHUB_CLIENT_ID == "" {
constants.GITHUB_CLIENT_ID = os.Getenv("GITHUB_CLIENT_ID") constants.EnvData.GITHUB_CLIENT_ID = os.Getenv("GITHUB_CLIENT_ID")
} }
if constants.GITHUB_CLIENT_SECRET == "" { if constants.EnvData.GITHUB_CLIENT_SECRET == "" {
constants.GITHUB_CLIENT_SECRET = os.Getenv("GITHUB_CLIENT_SECRET") constants.EnvData.GITHUB_CLIENT_SECRET = os.Getenv("GITHUB_CLIENT_SECRET")
} }
if constants.FACEBOOK_CLIENT_ID == "" { if constants.EnvData.FACEBOOK_CLIENT_ID == "" {
constants.FACEBOOK_CLIENT_ID = os.Getenv("FACEBOOK_CLIENT_ID") constants.EnvData.FACEBOOK_CLIENT_ID = os.Getenv("FACEBOOK_CLIENT_ID")
} }
if constants.FACEBOOK_CLIENT_SECRET == "" { if constants.EnvData.FACEBOOK_CLIENT_SECRET == "" {
constants.FACEBOOK_CLIENT_SECRET = os.Getenv("FACEBOOK_CLIENT_SECRET") constants.EnvData.FACEBOOK_CLIENT_SECRET = os.Getenv("FACEBOOK_CLIENT_SECRET")
} }
if constants.RESET_PASSWORD_URL == "" { if constants.EnvData.RESET_PASSWORD_URL == "" {
constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/") constants.EnvData.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/")
} }
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION") == "true" constants.EnvData.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION") == "true"
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION") == "true" constants.EnvData.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION") == "true"
constants.DISABLE_MAGIC_LINK_LOGIN = os.Getenv("DISABLE_MAGIC_LINK_LOGIN") == "true" constants.EnvData.DISABLE_MAGIC_LINK_LOGIN = os.Getenv("DISABLE_MAGIC_LINK_LOGIN") == "true"
constants.DISABLE_LOGIN_PAGE = os.Getenv("DISABLE_LOGIN_PAGE") == "true" constants.EnvData.DISABLE_LOGIN_PAGE = os.Getenv("DISABLE_LOGIN_PAGE") == "true"
if constants.SMTP_HOST == "" || constants.SMTP_USERNAME == "" || constants.SMTP_PASSWORD == "" || constants.SENDER_EMAIL == "" { if constants.EnvData.SMTP_HOST == "" || constants.EnvData.SMTP_USERNAME == "" || constants.EnvData.SMTP_PASSWORD == "" || constants.EnvData.SENDER_EMAIL == "" {
constants.DISABLE_EMAIL_VERIFICATION = true constants.EnvData.DISABLE_EMAIL_VERIFICATION = true
constants.DISABLE_MAGIC_LINK_LOGIN = true constants.EnvData.DISABLE_MAGIC_LINK_LOGIN = true
} }
allowedOriginsSplit := strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",") allowedOriginsSplit := strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",")
@ -209,18 +216,10 @@ func InitEnv() {
allowedOrigins = []string{"*"} allowedOrigins = []string{"*"}
} }
constants.ALLOWED_ORIGINS = allowedOrigins constants.EnvData.ALLOWED_ORIGINS = allowedOrigins
if constants.JWT_TYPE == "" { if constants.EnvData.DISABLE_EMAIL_VERIFICATION {
constants.JWT_TYPE = "HS256" constants.EnvData.DISABLE_MAGIC_LINK_LOGIN = true
}
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")) rolesEnv := strings.TrimSpace(os.Getenv("ROLES"))
@ -260,19 +259,19 @@ func InitEnv() {
} }
} }
if len(roles) > 0 && len(defaultRoles) == 0 && len(defaultRoleSplit) > 0 { if len(roles) > 0 && len(defaultRoles) == 0 && len(defaultRolesEnv) > 0 {
panic(`Invalid DEFAULT_ROLE environment variable. It can be one from give ROLES environment variable value`) panic(`Invalid DEFAULT_ROLE environment variable. It can be one from give ROLES environment variable value`)
} }
constants.ROLES = roles constants.EnvData.ROLES = roles
constants.DEFAULT_ROLES = defaultRoles constants.EnvData.DEFAULT_ROLES = defaultRoles
constants.PROTECTED_ROLES = protectedRoles constants.EnvData.PROTECTED_ROLES = protectedRoles
if os.Getenv("ORGANIZATION_NAME") != "" { if os.Getenv("ORGANIZATION_NAME") != "" {
constants.ORGANIZATION_NAME = os.Getenv("ORGANIZATION_NAME") constants.EnvData.ORGANIZATION_NAME = os.Getenv("ORGANIZATION_NAME")
} }
if os.Getenv("ORGANIZATION_LOGO") != "" { if os.Getenv("ORGANIZATION_LOGO") != "" {
constants.ORGANIZATION_LOGO = os.Getenv("ORGANIZATION_LOGO") constants.EnvData.ORGANIZATION_LOGO = os.Getenv("ORGANIZATION_LOGO")
} }
} }

138
server/env/persist_env.go vendored Normal file
View File

@ -0,0 +1,138 @@
package env
import (
"encoding/json"
"log"
"os"
"reflect"
"strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/google/uuid"
)
func PersistEnv() error {
config, err := db.Mgr.GetConfig()
// config not found in db
if err != nil {
// AES encryption needs 32 bit key only, so we chop off last 4 characters from 36 bit uuid
hash := uuid.New().String()[:36-4]
constants.EnvData.ENCRYPTION_KEY = hash
encodedHash := utils.EncryptB64(hash)
configData, err := json.Marshal(constants.EnvData)
if err != nil {
return err
}
encryptedConfig, err := utils.EncryptAES(configData)
if err != nil {
return err
}
config = db.Config{
Hash: encodedHash,
Config: encryptedConfig,
}
db.Mgr.AddConfig(config)
} else {
// decrypt the config data from db
// decryption can be done using the hash stored in db
encryptionKey := config.Hash
decryptedEncryptionKey, err := utils.DecryptB64(encryptionKey)
if err != nil {
return err
}
constants.EnvData.ENCRYPTION_KEY = decryptedEncryptionKey
decryptedConfigs, err := utils.DecryptAES(config.Config)
if err != nil {
return err
}
// temp json to validate with env
var jsonData map[string]interface{}
err = json.Unmarshal(decryptedConfigs, &jsonData)
if err != nil {
return err
}
// if env is changed via env file or OS env
// give that higher preference and update db, but we don't recommend it
hasChanged := false
for key, value := range jsonData {
fieldType := reflect.TypeOf(value).String()
// check only for derivative keys
// No need to check for ENCRYPTION_KEY which special key we use for encrypting config data
// as we have removed it from json
envValue := strings.TrimSpace(os.Getenv(key))
// env is not empty
if envValue != "" {
// check the type
// currently we have 3 types of env vars: string, bool, []string{}
if fieldType == "string" {
if value != envValue {
jsonData[key] = envValue
hasChanged = true
}
}
if fieldType == "bool" {
newValue := envValue == "true"
if value != newValue {
jsonData[key] = newValue
hasChanged = true
}
}
if fieldType == "[]interface {}" {
stringArr := []string{}
envStringArr := strings.Split(envValue, ",")
for _, v := range value.([]interface{}) {
stringArr = append(stringArr, v.(string))
}
if !utils.IsStringArrayEqual(stringArr, envStringArr) {
jsonData[key] = envStringArr
}
}
}
}
// handle derivative cases like disabling email verification & magic login
// in case SMTP is off but env is set to true
if jsonData["SMTP_HOST"] == "" || jsonData["SENDER_EMAIL"] == "" || jsonData["SENDER_PASSWORD"] == "" {
if !jsonData["DISABLE_EMAIL_VERIFICATION"].(bool) {
jsonData["DISABLE_EMAIL_VERIFICATION"] = true
hasChanged = true
}
if !jsonData["DISABLE_MAGIC_LINK_LOGIN"].(bool) {
jsonData["DISABLE_MAGIC_LINK_LOGIN"] = true
hasChanged = true
}
}
if hasChanged {
encryptedConfig, err := utils.EncryptConfig(jsonData)
if err != nil {
return err
}
config.Config = encryptedConfig
_, err = db.Mgr.UpdateConfig(config)
if err != nil {
log.Println("error updating config:", err)
return err
}
}
}
return nil
}

View File

@ -20,7 +20,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f 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.2.0 github.com/vektah/gqlparser/v2 v2.2.0
go.mongodb.org/mongo-driver v1.8.1 go.mongodb.org/mongo-driver v1.8.1
@ -30,7 +29,7 @@ require (
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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/mysql v1.2.1 gorm.io/driver/mysql v1.2.1
gorm.io/driver/postgres v1.2.3 gorm.io/driver/postgres v1.2.3

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,14 @@
package model package model
type AdminLoginInput struct {
AdminSecret string `json:"admin_secret"`
}
type AdminSignupInput struct {
AdminSecret string `json:"admin_secret"`
}
type AuthResponse struct { type AuthResponse struct {
Message string `json:"message"` Message string `json:"message"`
AccessToken *string `json:"access_token"` AccessToken *string `json:"access_token"`
@ -9,6 +17,42 @@ type AuthResponse struct {
User *User `json:"user"` User *User `json:"user"`
} }
type Config struct {
AdminSecret *string `json:"ADMIN_SECRET"`
DatabaseType *string `json:"DATABASE_TYPE"`
DatabaseURL *string `json:"DATABASE_URL"`
DatabaseName *string `json:"DATABASE_NAME"`
SMTPHost *string `json:"SMTP_HOST"`
SMTPPort *string `json:"SMTP_PORT"`
SMTPUsername *string `json:"SMTP_USERNAME"`
SMTPPassword *string `json:"SMTP_PASSWORD"`
SenderEmail *string `json:"SENDER_EMAIL"`
JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AuthorizerURL *string `json:"AUTHORIZER_URL"`
AppURL *string `json:"APP_URL"`
RedisURL *string `json:"REDIS_URL"`
CookieName *string `json:"COOKIE_NAME"`
ResetPasswordURL *string `json:"RESET_PASSWORD_URL"`
DisableEmailVerification *bool `json:"DISABLE_EMAIL_VERIFICATION"`
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
Roles []string `json:"ROLES"`
ProtectedRoles []string `json:"PROTECTED_ROLES"`
DefaultRoles []string `json:"DEFAULT_ROLES"`
JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"`
GoogleClientID *string `json:"GOOGLE_CLIENT_ID"`
GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"`
GithubClientID *string `json:"GITHUB_CLIENT_ID"`
GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"`
FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"`
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
}
type DeleteUserInput struct { type DeleteUserInput struct {
Email string `json:"email"` Email string `json:"email"`
} }
@ -73,6 +117,41 @@ type SignUpInput struct {
Roles []string `json:"roles"` Roles []string `json:"roles"`
} }
type UpdateConfigInput struct {
AdminSecret *string `json:"ADMIN_SECRET"`
DatabaseType *string `json:"DATABASE_TYPE"`
DatabaseURL *string `json:"DATABASE_URL"`
DatabaseName *string `json:"DATABASE_NAME"`
SMTPHost *string `json:"SMTP_HOST"`
SMTPPort *string `json:"SMTP_PORT"`
SenderEmail *string `json:"SENDER_EMAIL"`
SenderPassword *string `json:"SENDER_PASSWORD"`
JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AuthorizerURL *string `json:"AUTHORIZER_URL"`
AppURL *string `json:"APP_URL"`
RedisURL *string `json:"REDIS_URL"`
CookieName *string `json:"COOKIE_NAME"`
ResetPasswordURL *string `json:"RESET_PASSWORD_URL"`
DisableEmailVerification *bool `json:"DISABLE_EMAIL_VERIFICATION"`
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
Roles []string `json:"ROLES"`
ProtectedRoles []string `json:"PROTECTED_ROLES"`
DefaultRoles []string `json:"DEFAULT_ROLES"`
JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"`
GoogleClientID *string `json:"GOOGLE_CLIENT_ID"`
GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"`
GithubClientID *string `json:"GITHUB_CLIENT_ID"`
GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"`
FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"`
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
}
type UpdateProfileInput struct { type UpdateProfileInput struct {
OldPassword *string `json:"old_password"` OldPassword *string `json:"old_password"`
NewPassword *string `json:"new_password"` NewPassword *string `json:"new_password"`

View File

@ -62,6 +62,85 @@ type Response {
message: String! message: String!
} }
type Config {
ADMIN_SECRET: String
DATABASE_TYPE: String
DATABASE_URL: String
DATABASE_NAME: String
SMTP_HOST: String
SMTP_PORT: String
SMTP_USERNAME: String
SMTP_PASSWORD: String
SENDER_EMAIL: String
JWT_TYPE: String
JWT_SECRET: String
ALLOWED_ORIGINS: [String!]
AUTHORIZER_URL: String
APP_URL: String
REDIS_URL: String
COOKIE_NAME: String
RESET_PASSWORD_URL: String
DISABLE_EMAIL_VERIFICATION: Boolean
DISABLE_BASIC_AUTHENTICATION: Boolean
DISABLE_MAGIC_LINK_LOGIN: Boolean
DISABLE_LOGIN_PAGE: Boolean
ROLES: [String!]
PROTECTED_ROLES: [String!]
DEFAULT_ROLES: [String!]
JWT_ROLE_CLAIM: String
GOOGLE_CLIENT_ID: String
GOOGLE_CLIENT_SECRET: String
GITHUB_CLIENT_ID: String
GITHUB_CLIENT_SECRET: String
FACEBOOK_CLIENT_ID: String
FACEBOOK_CLIENT_SECRET: String
ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String
}
input UpdateConfigInput {
ADMIN_SECRET: String
DATABASE_TYPE: String
DATABASE_URL: String
DATABASE_NAME: String
SMTP_HOST: String
SMTP_PORT: String
SENDER_EMAIL: String
SENDER_PASSWORD: String
JWT_TYPE: String
JWT_SECRET: String
ALLOWED_ORIGINS: [String!]
AUTHORIZER_URL: String
APP_URL: String
REDIS_URL: String
COOKIE_NAME: String
RESET_PASSWORD_URL: String
DISABLE_EMAIL_VERIFICATION: Boolean
DISABLE_BASIC_AUTHENTICATION: Boolean
DISABLE_MAGIC_LINK_LOGIN: Boolean
DISABLE_LOGIN_PAGE: Boolean
ROLES: [String!]
PROTECTED_ROLES: [String!]
DEFAULT_ROLES: [String!]
JWT_ROLE_CLAIM: String
GOOGLE_CLIENT_ID: String
GOOGLE_CLIENT_SECRET: String
GITHUB_CLIENT_ID: String
GITHUB_CLIENT_SECRET: String
FACEBOOK_CLIENT_ID: String
FACEBOOK_CLIENT_SECRET: String
ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String
}
input AdminLoginInput {
admin_secret: String!
}
input AdminSignupInput {
admin_secret: String!
}
input SignUpInput { input SignUpInput {
email: String! email: String!
given_name: String given_name: String
@ -153,13 +232,19 @@ type Mutation {
# admin only apis # admin only apis
_delete_user(params: DeleteUserInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User! _update_user(params: UpdateUserInput!): User!
_admin_signup(params: AdminSignupInput!): Response!
_admin_login(params: AdminLoginInput!): Response!
_admin_logout: Response!
_update_config(params: UpdateConfigInput!): Response!
} }
type Query { type Query {
meta: Meta! meta: Meta!
session(roles: [String!]): AuthResponse session(roles: [String!]): AuthResponse!
profile: User! profile: User!
# admin only apis # admin only apis
_users: [User!]! _users: [User!]!
_verification_requests: [VerificationRequest!]! _verification_requests: [VerificationRequest!]!
_admin_session: Response!
_config: Config!
} }

View File

@ -55,6 +55,22 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, params model.UpdateUs
return resolvers.UpdateUser(ctx, params) return resolvers.UpdateUser(ctx, params)
} }
func (r *mutationResolver) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) {
return resolvers.AdminSignupResolver(ctx, params)
}
func (r *mutationResolver) AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error) {
return resolvers.AdminLoginResolver(ctx, params)
}
func (r *mutationResolver) AdminLogout(ctx context.Context) (*model.Response, error) {
return resolvers.AdminLogout(ctx)
}
func (r *mutationResolver) UpdateConfig(ctx context.Context, params model.UpdateConfigInput) (*model.Response, error) {
return resolvers.UpdateConfigResolver(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)
} }
@ -75,13 +91,19 @@ func (r *queryResolver) VerificationRequests(ctx context.Context) ([]*model.Veri
return resolvers.VerificationRequests(ctx) return resolvers.VerificationRequests(ctx)
} }
func (r *queryResolver) AdminSession(ctx context.Context) (*model.Response, error) {
return resolvers.AdminSession(ctx)
}
func (r *queryResolver) Config(ctx context.Context) (*model.Config, error) {
return resolvers.ConfigResolver(ctx)
}
// Mutation returns generated.MutationResolver implementation. // Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// 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

@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
@ -30,17 +29,17 @@ func AppHandler() gin.HandlerFunc {
// return // return
// } // }
stateObj.AuthorizerURL = constants.AUTHORIZER_URL stateObj.AuthorizerURL = constants.EnvData.AUTHORIZER_URL
stateObj.RedirectURL = constants.AUTHORIZER_URL + "/app" stateObj.RedirectURL = constants.EnvData.AUTHORIZER_URL + "/app"
} else { } else {
decodedState, err := base64.StdEncoding.DecodeString(state) decodedState, err := utils.DecryptB64(state)
if err != nil { if err != nil {
c.JSON(400, gin.H{"error": "[unable to decode state] invalid state"}) c.JSON(400, gin.H{"error": "[unable to decode state] invalid state"})
return return
} }
err = json.Unmarshal(decodedState, &stateObj) err = json.Unmarshal([]byte(decodedState), &stateObj)
if err != nil { if err != nil {
c.JSON(400, gin.H{"error": "[unable to parse state] invalid state"}) c.JSON(400, gin.H{"error": "[unable to parse state] invalid state"})
return return
@ -60,7 +59,7 @@ func AppHandler() gin.HandlerFunc {
} }
// validate host and domain of authorizer url // validate host and domain of authorizer url
if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != constants.AUTHORIZER_URL { if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != constants.EnvData.AUTHORIZER_URL {
c.JSON(400, gin.H{"error": "invalid host url"}) c.JSON(400, gin.H{"error": "invalid host url"})
return return
} }
@ -77,8 +76,8 @@ func AppHandler() gin.HandlerFunc {
"data": map[string]string{ "data": map[string]string{
"authorizerURL": stateObj.AuthorizerURL, "authorizerURL": stateObj.AuthorizerURL,
"redirectURL": stateObj.RedirectURL, "redirectURL": stateObj.RedirectURL,
"organizationName": constants.ORGANIZATION_NAME, "organizationName": constants.EnvData.ORGANIZATION_NAME,
"organizationLogo": constants.ORGANIZATION_LOGO, "organizationLogo": constants.EnvData.ORGANIZATION_LOGO,
}, },
}) })
} }

View File

@ -0,0 +1,24 @@
package handlers
import (
"net/http"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/gin-gonic/gin"
)
func DashboardHandler() gin.HandlerFunc {
return func(c *gin.Context) {
isOnboardingCompleted := false
if constants.EnvData.ADMIN_SECRET != "" {
isOnboardingCompleted = true
}
c.HTML(http.StatusOK, "dashboard.tmpl", gin.H{
"data": map[string]interface{}{
"isOnboardingCompleted": isOnboardingCompleted,
},
})
}
}

View File

@ -195,7 +195,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
// 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 {
if utils.StringSliceContains(constants.PROTECTED_ROLES, ir) { if utils.StringSliceContains(constants.EnvData.PROTECTED_ROLES, ir) {
hasProtectedRole = true hasProtectedRole = true
} }
} }
@ -238,7 +238,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
// check if it contains protected unassigned role // check if it contains protected unassigned role
hasProtectedRole := false hasProtectedRole := false
for _, ur := range unasignedRoles { for _, ur := range unasignedRoles {
if utils.StringSliceContains(constants.PROTECTED_ROLES, ur) { if utils.StringSliceContains(constants.EnvData.PROTECTED_ROLES, ur) {
hasProtectedRole = true hasProtectedRole = true
} }
} }

View File

@ -34,14 +34,14 @@ func OAuthLoginHandler() gin.HandlerFunc {
// use protected roles verification for admin login only. // use protected roles verification for admin login only.
// though if not associated with user, it will be rejected from oauth_callback // though if not associated with user, it will be rejected from oauth_callback
if !utils.IsValidRoles(append([]string{}, append(constants.ROLES, constants.PROTECTED_ROLES...)...), rolesSplit) { if !utils.IsValidRoles(append([]string{}, append(constants.EnvData.ROLES, constants.EnvData.PROTECTED_ROLES...)...), rolesSplit) {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid role", "error": "invalid role",
}) })
return return
} }
} else { } else {
roles = strings.Join(constants.DEFAULT_ROLES, ",") roles = strings.Join(constants.EnvData.DEFAULT_ROLES, ",")
} }
uuid := uuid.New() uuid := uuid.New()
@ -57,7 +57,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
} }
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.OAuthProviders.GoogleConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/google" oauth.OAuthProviders.GoogleConfig.RedirectURL = constants.EnvData.AUTHORIZER_URL + "/oauth_callback/google"
url := oauth.OAuthProviders.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():
@ -66,7 +66,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
break break
} }
session.SetSocailLoginState(oauthStateString, enum.Github.String()) session.SetSocailLoginState(oauthStateString, enum.Github.String())
oauth.OAuthProviders.GithubConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/github" oauth.OAuthProviders.GithubConfig.RedirectURL = constants.EnvData.AUTHORIZER_URL + "/oauth_callback/github"
url := oauth.OAuthProviders.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():
@ -75,7 +75,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
break break
} }
session.SetSocailLoginState(oauthStateString, enum.Facebook.String()) session.SetSocailLoginState(oauthStateString, enum.Facebook.String())
oauth.OAuthProviders.FacebookConfig.RedirectURL = constants.AUTHORIZER_URL + "/oauth_callback/facebook" oauth.OAuthProviders.FacebookConfig.RedirectURL = constants.EnvData.AUTHORIZER_URL + "/oauth_callback/facebook"
url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
default: default:

View File

@ -22,20 +22,22 @@ func main() {
env.ARG_ENV_FILE = flag.String("env_file", "", "Env file path") env.ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
flag.Parse() flag.Parse()
constants.VERSION = VERSION constants.EnvData.VERSION = VERSION
env.InitEnv() env.InitEnv()
db.InitDB() db.InitDB()
env.PersistEnv()
session.InitSession() session.InitSession()
oauth.InitOAuth() oauth.InitOAuth()
utils.InitServer() utils.InitServer()
router := router.InitRouter() router := router.InitRouter()
router.LoadHTMLGlob("templates/*")
// login page app related routes. // login page app related routes.
// if we put them in router file then tests would fail as templates or build path will be different // if we put them in router file then tests would fail as templates or build path will be different
if !constants.DISABLE_LOGIN_PAGE { if !constants.EnvData.DISABLE_LOGIN_PAGE {
router.LoadHTMLGlob("templates/*")
app := router.Group("/app") app := router.Group("/app")
{ {
app.Static("/build", "app/build") app.Static("/build", "app/build")
@ -44,5 +46,11 @@ func main() {
} }
} }
router.Run(":" + constants.PORT) app := router.Group("/dashboard")
{
app.Static("/build", "dashboard/build")
app.GET("/", handlers.DashboardHandler())
}
router.Run(":" + constants.EnvData.PORT)
} }

View File

@ -11,10 +11,10 @@ import (
func GinContextToContextMiddleware() gin.HandlerFunc { func GinContextToContextMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if constants.AUTHORIZER_URL == "" { if constants.EnvData.AUTHORIZER_URL == "" {
url := location.Get(c) url := location.Get(c)
constants.AUTHORIZER_URL = url.Scheme + "://" + c.Request.Host constants.EnvData.AUTHORIZER_URL = url.Scheme + "://" + c.Request.Host
log.Println("authorizer url:", constants.AUTHORIZER_URL) log.Println("authorizer url:", constants.EnvData.AUTHORIZER_URL)
} }
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c) ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
c.Request = c.Request.WithContext(ctx) c.Request = c.Request.WithContext(ctx)

View File

@ -1,7 +1,6 @@
package middlewares package middlewares
import ( import (
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -9,7 +8,6 @@ import (
func CORSMiddleware() gin.HandlerFunc { func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin") origin := c.Request.Header.Get("Origin")
constants.APP_URL = origin
if utils.IsValidOrigin(origin) { if utils.IsValidOrigin(origin) {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin) c.Writer.Header().Set("Access-Control-Allow-Origin", origin)

View File

@ -28,33 +28,33 @@ var (
func InitOAuth() { func InitOAuth() {
ctx := context.Background() ctx := context.Background()
if constants.GOOGLE_CLIENT_ID != "" && constants.GOOGLE_CLIENT_SECRET != "" { if constants.EnvData.GOOGLE_CLIENT_ID != "" && constants.EnvData.GOOGLE_CLIENT_SECRET != "" {
p, err := oidc.NewProvider(ctx, "https://accounts.google.com") p, err := oidc.NewProvider(ctx, "https://accounts.google.com")
if err != nil { if err != nil {
log.Fatalln("error creating oidc provider for google:", err) log.Fatalln("error creating oidc provider for google:", err)
} }
OIDCProviders.GoogleOIDC = p OIDCProviders.GoogleOIDC = p
OAuthProviders.GoogleConfig = &oauth2.Config{ OAuthProviders.GoogleConfig = &oauth2.Config{
ClientID: constants.GOOGLE_CLIENT_ID, ClientID: constants.EnvData.GOOGLE_CLIENT_ID,
ClientSecret: constants.GOOGLE_CLIENT_SECRET, ClientSecret: constants.EnvData.GOOGLE_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/google", RedirectURL: constants.EnvData.AUTHORIZER_URL + "/oauth_callback/google",
Endpoint: OIDCProviders.GoogleOIDC.Endpoint(), Endpoint: OIDCProviders.GoogleOIDC.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
} }
} }
if constants.GITHUB_CLIENT_ID != "" && constants.GITHUB_CLIENT_SECRET != "" { if constants.EnvData.GITHUB_CLIENT_ID != "" && constants.EnvData.GITHUB_CLIENT_SECRET != "" {
OAuthProviders.GithubConfig = &oauth2.Config{ OAuthProviders.GithubConfig = &oauth2.Config{
ClientID: constants.GITHUB_CLIENT_ID, ClientID: constants.EnvData.GITHUB_CLIENT_ID,
ClientSecret: constants.GITHUB_CLIENT_SECRET, ClientSecret: constants.EnvData.GITHUB_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/github", RedirectURL: constants.EnvData.AUTHORIZER_URL + "/oauth_callback/github",
Endpoint: githubOAuth2.Endpoint, Endpoint: githubOAuth2.Endpoint,
} }
} }
if constants.FACEBOOK_CLIENT_ID != "" && constants.FACEBOOK_CLIENT_SECRET != "" { if constants.EnvData.FACEBOOK_CLIENT_ID != "" && constants.EnvData.FACEBOOK_CLIENT_SECRET != "" {
OAuthProviders.FacebookConfig = &oauth2.Config{ OAuthProviders.FacebookConfig = &oauth2.Config{
ClientID: constants.FACEBOOK_CLIENT_ID, ClientID: constants.EnvData.FACEBOOK_CLIENT_ID,
ClientSecret: constants.FACEBOOK_CLIENT_SECRET, ClientSecret: constants.EnvData.FACEBOOK_CLIENT_SECRET,
RedirectURL: constants.AUTHORIZER_URL + "/oauth_callback/facebook", RedirectURL: constants.EnvData.AUTHORIZER_URL + "/oauth_callback/facebook",
Endpoint: facebookOAuth2.Endpoint, Endpoint: facebookOAuth2.Endpoint,
Scopes: []string{"public_profile", "email"}, Scopes: []string{"public_profile", "email"},
} }

View File

@ -0,0 +1,34 @@
package resolvers
import (
"context"
"fmt"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils"
)
func AdminLoginResolver(ctx context.Context, params model.AdminLoginInput) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
var res *model.Response
if err != nil {
return res, err
}
if params.AdminSecret != constants.EnvData.ADMIN_SECRET {
return res, fmt.Errorf(`invalid admin secret`)
}
hashedKey, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
if err != nil {
return res, err
}
utils.SetAdminCookie(gc, hashedKey)
res = &model.Response{
Message: "admin logged in successfully",
}
return res, nil
}

View File

@ -0,0 +1,29 @@
package resolvers
import (
"context"
"fmt"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils"
)
func AdminLogout(ctx context.Context) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
var res *model.Response
if err != nil {
return res, err
}
if !utils.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized")
}
utils.DeleteAdminCookie(gc)
res = &model.Response{
Message: "admin logged out successfully",
}
return res, nil
}

View File

@ -0,0 +1,34 @@
package resolvers
import (
"context"
"fmt"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils"
)
func AdminSession(ctx context.Context) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
var res *model.Response
if err != nil {
return res, err
}
if !utils.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized")
}
hashedKey, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
if err != nil {
return res, err
}
utils.SetAdminCookie(gc, hashedKey)
res = &model.Response{
Message: "admin logged in successfully",
}
return res, nil
}

View File

@ -0,0 +1,77 @@
package resolvers
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils"
)
func AdminSignupResolver(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
var res *model.Response
if err != nil {
return res, err
}
if strings.TrimSpace(params.AdminSecret) == "" {
err = fmt.Errorf("please select secure admin secret")
return res, err
}
if len(params.AdminSecret) < 6 {
err = fmt.Errorf("admin secret must be at least 6 characters")
return res, err
}
if constants.EnvData.ADMIN_SECRET != "" {
err = fmt.Errorf("admin sign up already completed")
return res, err
}
constants.EnvData.ADMIN_SECRET = params.AdminSecret
// consvert EnvData to JSON
var jsonData map[string]interface{}
jsonBytes, err := json.Marshal(constants.EnvData)
if err != nil {
return res, err
}
if err := json.Unmarshal(jsonBytes, &jsonData); err != nil {
return res, err
}
config, err := db.Mgr.GetConfig()
if err != nil {
return res, err
}
configData, err := utils.EncryptConfig(jsonData)
if err != nil {
return res, err
}
config.Config = configData
if _, err := db.Mgr.UpdateConfig(config); err != nil {
return res, err
}
hashedKey, err := utils.HashPassword(params.AdminSecret)
if err != nil {
return res, err
}
utils.SetAdminCookie(gc, hashedKey)
res = &model.Response{
Message: "admin signed up successfully",
}
return res, nil
}

View File

@ -0,0 +1,60 @@
package resolvers
import (
"context"
"fmt"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils"
)
func ConfigResolver(ctx context.Context) (*model.Config, error) {
gc, err := utils.GinContextFromContext(ctx)
var res *model.Config
if err != nil {
return res, err
}
if !utils.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized")
}
res = &model.Config{
AdminSecret: &constants.EnvData.ADMIN_SECRET,
DatabaseType: &constants.EnvData.DATABASE_TYPE,
DatabaseURL: &constants.EnvData.DATABASE_URL,
DatabaseName: &constants.EnvData.DATABASE_NAME,
SMTPHost: &constants.EnvData.SMTP_HOST,
SMTPPort: &constants.EnvData.SMTP_PORT,
SMTPPassword: &constants.EnvData.SMTP_PASSWORD,
SMTPUsername: &constants.EnvData.SMTP_USERNAME,
SenderEmail: &constants.EnvData.SENDER_EMAIL,
JwtType: &constants.EnvData.JWT_TYPE,
JwtSecret: &constants.EnvData.JWT_SECRET,
AllowedOrigins: constants.EnvData.ALLOWED_ORIGINS,
AuthorizerURL: &constants.EnvData.AUTHORIZER_URL,
AppURL: &constants.EnvData.APP_URL,
RedisURL: &constants.EnvData.REDIS_URL,
CookieName: &constants.EnvData.COOKIE_NAME,
ResetPasswordURL: &constants.EnvData.RESET_PASSWORD_URL,
DisableEmailVerification: &constants.EnvData.DISABLE_EMAIL_VERIFICATION,
DisableBasicAuthentication: &constants.EnvData.DISABLE_BASIC_AUTHENTICATION,
DisableMagicLinkLogin: &constants.EnvData.DISABLE_MAGIC_LINK_LOGIN,
DisableLoginPage: &constants.EnvData.DISABLE_LOGIN_PAGE,
Roles: constants.EnvData.ROLES,
ProtectedRoles: constants.EnvData.PROTECTED_ROLES,
DefaultRoles: constants.EnvData.DEFAULT_ROLES,
JwtRoleClaim: &constants.EnvData.JWT_ROLE_CLAIM,
GoogleClientID: &constants.EnvData.GOOGLE_CLIENT_ID,
GoogleClientSecret: &constants.EnvData.GOOGLE_CLIENT_SECRET,
GithubClientID: &constants.EnvData.GITHUB_CLIENT_ID,
GithubClientSecret: &constants.EnvData.GITHUB_CLIENT_SECRET,
FacebookClientID: &constants.EnvData.FACEBOOK_CLIENT_ID,
FacebookClientSecret: &constants.EnvData.FACEBOOK_CLIENT_SECRET,
OrganizationName: &constants.EnvData.ORGANIZATION_NAME,
OrganizationLogo: &constants.EnvData.ORGANIZATION_LOGO,
}
return res, nil
}

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 { if constants.EnvData.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

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 { if constants.EnvData.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`)
} }
@ -46,7 +46,7 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
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.EnvData.DEFAULT_ROLES
currentRoles := strings.Split(user.Roles, ",") currentRoles := strings.Split(user.Roles, ",")
if len(params.Roles) > 0 { if len(params.Roles) > 0 {
if !utils.IsValidRoles(currentRoles, params.Roles) { if !utils.IsValidRoles(currentRoles, params.Roles) {

View File

@ -17,7 +17,7 @@ import (
func MagicLinkLogin(ctx context.Context, params model.MagicLinkLoginInput) (*model.Response, error) { func MagicLinkLogin(ctx context.Context, params model.MagicLinkLoginInput) (*model.Response, error) {
var res *model.Response var res *model.Response
if constants.DISABLE_MAGIC_LINK_LOGIN { if constants.EnvData.DISABLE_MAGIC_LINK_LOGIN {
return res, fmt.Errorf(`magic link login is disabled for this instance`) return res, fmt.Errorf(`magic link login is disabled for this instance`)
} }
@ -41,13 +41,13 @@ func MagicLinkLogin(ctx context.Context, params model.MagicLinkLoginInput) (*mod
// define roles for new user // define roles for new user
if len(params.Roles) > 0 { if len(params.Roles) > 0 {
// check if roles exists // check if roles exists
if !utils.IsValidRoles(constants.ROLES, params.Roles) { if !utils.IsValidRoles(constants.EnvData.ROLES, params.Roles) {
return res, fmt.Errorf(`invalid roles`) return res, fmt.Errorf(`invalid roles`)
} else { } else {
inputRoles = params.Roles inputRoles = params.Roles
} }
} else { } else {
inputRoles = constants.DEFAULT_ROLES inputRoles = constants.EnvData.DEFAULT_ROLES
} }
user.Roles = strings.Join(inputRoles, ",") user.Roles = strings.Join(inputRoles, ",")
@ -72,7 +72,7 @@ func MagicLinkLogin(ctx context.Context, params model.MagicLinkLoginInput) (*mod
// check if it contains protected unassigned role // check if it contains protected unassigned role
hasProtectedRole := false hasProtectedRole := false
for _, ur := range unasignedRoles { for _, ur := range unasignedRoles {
if utils.StringSliceContains(constants.PROTECTED_ROLES, ur) { if utils.StringSliceContains(constants.EnvData.PROTECTED_ROLES, ur) {
hasProtectedRole = true hasProtectedRole = true
} }
} }
@ -98,7 +98,7 @@ func MagicLinkLogin(ctx context.Context, params model.MagicLinkLoginInput) (*mod
} }
} }
if !constants.DISABLE_EMAIL_VERIFICATION { if !constants.EnvData.DISABLE_EMAIL_VERIFICATION {
// insert verification request // insert verification request
verificationType := enum.MagicLinkLogin.String() verificationType := enum.MagicLinkLogin.String()
token, err := utils.CreateVerificationToken(params.Email, verificationType) token, err := utils.CreateVerificationToken(params.Email, verificationType)

View File

@ -15,7 +15,7 @@ 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 { if constants.EnvData.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`)
} }

View File

@ -45,7 +45,7 @@ func Session(ctx context.Context, roles []string) (*model.AuthResponse, error) {
expiresTimeObj := time.Unix(expiresAt, 0) expiresTimeObj := time.Unix(expiresAt, 0)
currentTimeObj := time.Now() currentTimeObj := time.Now()
claimRoleInterface := claim[constants.JWT_ROLE_CLAIM].([]interface{}) claimRoleInterface := claim[constants.EnvData.JWT_ROLE_CLAIM].([]interface{})
claimRoles := make([]string, len(claimRoleInterface)) claimRoles := make([]string, len(claimRoleInterface))
for i, v := range claimRoleInterface { for i, v := range claimRoleInterface {
claimRoles[i] = v.(string) claimRoles[i] = v.(string)

View File

@ -22,7 +22,7 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
return res, err return res, err
} }
if constants.DISABLE_BASIC_AUTHENTICATION { if constants.EnvData.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 {
@ -52,13 +52,13 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
if len(params.Roles) > 0 { if len(params.Roles) > 0 {
// check if roles exists // check if roles exists
if !utils.IsValidRoles(constants.ROLES, params.Roles) { if !utils.IsValidRoles(constants.EnvData.ROLES, params.Roles) {
return res, fmt.Errorf(`invalid roles`) return res, fmt.Errorf(`invalid roles`)
} else { } else {
inputRoles = params.Roles inputRoles = params.Roles
} }
} else { } else {
inputRoles = constants.DEFAULT_ROLES inputRoles = constants.EnvData.DEFAULT_ROLES
} }
user := db.User{ user := db.User{
@ -103,7 +103,7 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
} }
user.SignupMethods = enum.BasicAuth.String() user.SignupMethods = enum.BasicAuth.String()
if constants.DISABLE_EMAIL_VERIFICATION { if constants.EnvData.DISABLE_EMAIL_VERIFICATION {
now := time.Now().Unix() now := time.Now().Unix()
user.EmailVerifiedAt = &now user.EmailVerifiedAt = &now
} }
@ -115,7 +115,7 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
userToReturn := utils.GetResponseUserData(user) userToReturn := utils.GetResponseUserData(user)
if !constants.DISABLE_EMAIL_VERIFICATION { if !constants.EnvData.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)

View File

@ -0,0 +1,105 @@
package resolvers
import (
"context"
"encoding/json"
"fmt"
"log"
"reflect"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils"
)
func UpdateConfigResolver(ctx context.Context, params model.UpdateConfigInput) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
var res *model.Response
if err != nil {
return res, err
}
if !utils.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized")
}
var data map[string]interface{}
byteData, err := json.Marshal(params)
if err != nil {
return res, fmt.Errorf("error marshalling params: %t", err)
}
err = json.Unmarshal(byteData, &data)
if err != nil {
return res, fmt.Errorf("error un-marshalling params: %t", err)
}
updatedData := make(map[string]interface{})
for key, value := range data {
if value != nil {
fieldType := reflect.TypeOf(value).String()
if fieldType == "string" || fieldType == "bool" {
updatedData[key] = value
}
if fieldType == "[]interface {}" {
stringArr := []string{}
for _, v := range value.([]interface{}) {
stringArr = append(stringArr, v.(string))
}
updatedData[key] = stringArr
}
}
}
// handle derivative cases like disabling email verification & magic login
// in case SMTP is off but env is set to true
if updatedData["SMTP_HOST"] == "" || updatedData["SENDER_EMAIL"] == "" || updatedData["SENDER_PASSWORD"] == "" {
if !updatedData["DISABLE_EMAIL_VERIFICATION"].(bool) {
updatedData["DISABLE_EMAIL_VERIFICATION"] = true
}
if !updatedData["DISABLE_MAGIC_LINK_LOGIN"].(bool) {
updatedData["DISABLE_MAGIC_LINK_LOGIN"] = true
}
}
config, err := db.Mgr.GetConfig()
if err != nil {
return res, err
}
encryptedConfig, err := utils.EncryptConfig(updatedData)
if err != nil {
return res, err
}
// in case of db change re-initialize db
if params.DatabaseType != nil || params.DatabaseURL != nil || params.DatabaseName != nil {
db.InitDB()
}
// in case of admin secret change update the cookie with new hash
if params.AdminSecret != nil {
hashedKey, err := utils.HashPassword(constants.EnvData.ADMIN_SECRET)
if err != nil {
return res, err
}
utils.SetAdminCookie(gc, hashedKey)
}
config.Config = encryptedConfig
_, err = db.Mgr.UpdateConfig(config)
if err != nil {
log.Println("error updating config:", err)
return res, err
}
res = &model.Response{
Message: "configurations updated successfully",
}
return res, nil
}

View File

@ -112,7 +112,7 @@ func UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User,
inputRoles = append(inputRoles, *item) inputRoles = append(inputRoles, *item)
} }
if !utils.IsValidRoles(append([]string{}, append(constants.ROLES, constants.PROTECTED_ROLES...)...), inputRoles) { if !utils.IsValidRoles(append([]string{}, append(constants.EnvData.ROLES, constants.EnvData.PROTECTED_ROLES...)...), inputRoles) {
return res, fmt.Errorf("invalid list of roles") return res, fmt.Errorf("invalid list of roles")
} }

View File

@ -95,9 +95,9 @@ func RemoveSocialLoginState(key string) {
} }
func InitSession() { func InitSession() {
if constants.REDIS_URL != "" { if constants.EnvData.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.EnvData.REDIS_URL)
if err != nil { if err != nil {
log.Fatalln("Error parsing redis url:", err) log.Fatalln("Error parsing redis url:", err)
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"net/url"
"os" "os"
"strings" "strings"
"time" "time"
@ -14,10 +15,11 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
"golang.org/x/crypto/bcrypt"
) )
func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (string, int64, error) { func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (string, int64, error) {
t := jwt.New(jwt.GetSigningMethod(constants.JWT_TYPE)) t := jwt.New(jwt.GetSigningMethod(constants.EnvData.JWT_TYPE))
expiryBound := time.Hour expiryBound := time.Hour
if tokenType == enum.RefreshToken { if tokenType == enum.RefreshToken {
// expires in 1 year // expires in 1 year
@ -32,11 +34,11 @@ func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (st
json.Unmarshal(userBytes, &userMap) 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(),
"allowed_roles": strings.Split(user.Roles, ","), "allowed_roles": strings.Split(user.Roles, ","),
constants.JWT_ROLE_CLAIM: roles, constants.EnvData.JWT_ROLE_CLAIM: roles,
} }
for k, v := range userMap { for k, v := range userMap {
@ -77,7 +79,7 @@ func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (st
t.Claims = customClaims t.Claims = customClaims
token, err := t.SignedString([]byte(constants.JWT_SECRET)) token, err := t.SignedString([]byte(constants.EnvData.JWT_SECRET))
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
@ -89,7 +91,6 @@ func GetAuthToken(gc *gin.Context) (string, error) {
token, err := GetCookie(gc) token, err := GetCookie(gc)
if err != nil || token == "" { if err != nil || token == "" {
// try to check in auth header for cookie // try to check in auth header for cookie
log.Println("cookie not found checking headers")
auth := gc.Request.Header.Get("Authorization") auth := gc.Request.Header.Get("Authorization")
if auth == "" { if auth == "" {
return "", fmt.Errorf(`unauthorized`) return "", fmt.Errorf(`unauthorized`)
@ -105,7 +106,7 @@ func VerifyAuthToken(token string) (map[string]interface{}, error) {
claims := jwt.MapClaims{} claims := jwt.MapClaims{}
_, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) { _, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(constants.JWT_SECRET), nil return []byte(constants.EnvData.JWT_SECRET), nil
}) })
if err != nil { if err != nil {
return res, err return res, err
@ -124,3 +125,29 @@ func VerifyAuthToken(token string) (map[string]interface{}, error) {
return res, nil return res, nil
} }
func CreateAdminAuthToken(tokenType enum.TokenType, c *gin.Context) (string, error) {
return HashPassword(constants.EnvData.ADMIN_SECRET)
}
func GetAdminAuthToken(gc *gin.Context) (string, error) {
token, err := GetAdminCookie(gc)
if err != nil || token == "" {
return "", fmt.Errorf("unauthorized")
}
// cookie escapes special characters like $
// hence we need to unescape before comparing
decodedValue, err := url.QueryUnescape(token)
if err != nil {
return "", err
}
err = bcrypt.CompareHashAndPassword([]byte(decodedValue), []byte(constants.EnvData.ADMIN_SECRET))
log.Println("error comparing hash:", err)
if err != nil {
return "", fmt.Errorf(`unauthorized`)
}
return token, nil
}

View File

@ -10,21 +10,21 @@ 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) host, _ := GetHostParts(constants.EnvData.AUTHORIZER_URL)
domain := GetDomainName(constants.AUTHORIZER_URL) domain := GetDomainName(constants.EnvData.AUTHORIZER_URL)
if domain != "localhost" { if domain != "localhost" {
domain = "." + domain domain = "." + domain
} }
gc.SetSameSite(http.SameSiteNoneMode) gc.SetSameSite(http.SameSiteNoneMode)
gc.SetCookie(constants.COOKIE_NAME, token, 3600, "/", host, secure, httpOnly) gc.SetCookie(constants.EnvData.COOKIE_NAME, token, 3600, "/", host, secure, httpOnly)
gc.SetCookie(constants.COOKIE_NAME+"-client", token, 3600, "/", domain, secure, httpOnly) gc.SetCookie(constants.EnvData.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.EnvData.COOKIE_NAME)
if err != nil { if err != nil {
cookie, err = gc.Request.Cookie(constants.COOKIE_NAME + "-client") cookie, err = gc.Request.Cookie(constants.EnvData.COOKIE_NAME + "-client")
if err != nil { if err != nil {
return "", err return "", err
} }
@ -37,13 +37,37 @@ func DeleteCookie(gc *gin.Context) {
secure := true secure := true
httpOnly := true httpOnly := true
host, _ := GetHostParts(constants.AUTHORIZER_URL) host, _ := GetHostParts(constants.EnvData.AUTHORIZER_URL)
domain := GetDomainName(constants.AUTHORIZER_URL) domain := GetDomainName(constants.EnvData.AUTHORIZER_URL)
if domain != "localhost" { if domain != "localhost" {
domain = "." + domain domain = "." + domain
} }
gc.SetSameSite(http.SameSiteNoneMode) gc.SetSameSite(http.SameSiteNoneMode)
gc.SetCookie(constants.COOKIE_NAME, "", -1, "/", host, secure, httpOnly) gc.SetCookie(constants.EnvData.COOKIE_NAME, "", -1, "/", host, secure, httpOnly)
gc.SetCookie(constants.COOKIE_NAME+"-client", "", -1, "/", domain, secure, httpOnly) gc.SetCookie(constants.EnvData.COOKIE_NAME+"-client", "", -1, "/", domain, secure, httpOnly)
}
func SetAdminCookie(gc *gin.Context, token string) {
secure := true
httpOnly := true
host, _ := GetHostParts(constants.EnvData.AUTHORIZER_URL)
gc.SetCookie(constants.EnvData.ADMIN_COOKIE_NAME, token, 3600, "/", host, secure, httpOnly)
}
func GetAdminCookie(gc *gin.Context) (string, error) {
cookie, err := gc.Request.Cookie(constants.EnvData.ADMIN_COOKIE_NAME)
if err != nil {
return "", err
}
return cookie.Value, nil
}
func DeleteAdminCookie(gc *gin.Context) {
secure := true
httpOnly := true
host, _ := GetHostParts(constants.EnvData.AUTHORIZER_URL)
gc.SetCookie(constants.EnvData.ADMIN_COOKIE_NAME, "", -1, "/", host, secure, httpOnly)
} }

83
server/utils/crypto.go Normal file
View File

@ -0,0 +1,83 @@
package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"
"github.com/authorizerdev/authorizer/server/constants"
)
func EncryptB64(text string) string {
return base64.StdEncoding.EncodeToString([]byte(text))
}
func DecryptB64(s string) (string, error) {
data, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(data), nil
}
func EncryptAES(text []byte) ([]byte, error) {
key := []byte(constants.EnvData.ENCRYPTION_KEY)
c, err := aes.NewCipher(key)
var res []byte
if err != nil {
return res, err
}
// gcm or Galois/Counter Mode, is a mode of operation
// for symmetric key cryptographic block ciphers
// - https://en.wikipedia.org/wiki/Galois/Counter_Mode
gcm, err := cipher.NewGCM(c)
if err != nil {
return res, err
}
// creates a new byte array the size of the nonce
// which must be passed to Seal
nonce := make([]byte, gcm.NonceSize())
// populates our nonce with a cryptographically secure
// random sequence
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return res, err
}
// here we encrypt our text using the Seal function
// Seal encrypts and authenticates plaintext, authenticates the
// additional data and appends the result to dst, returning the updated
// slice. The nonce must be NonceSize() bytes long and unique for all
// time, for a given key.
return gcm.Seal(nonce, nonce, text, nil), nil
}
func DecryptAES(ciphertext []byte) ([]byte, error) {
key := []byte(constants.EnvData.ENCRYPTION_KEY)
c, err := aes.NewCipher(key)
var res []byte
if err != nil {
return res, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return res, err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return res, err
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return res, err
}
return plaintext, nil
}

View File

@ -101,9 +101,9 @@ func SendVerificationMail(toEmail, token string) error {
</html> </html>
` `
data := make(map[string]interface{}, 3) data := make(map[string]interface{}, 3)
data["OrgLogo"] = constants.ORGANIZATION_LOGO data["OrgLogo"] = constants.EnvData.ORGANIZATION_LOGO
data["OrgName"] = constants.ORGANIZATION_NAME data["OrgName"] = constants.EnvData.ORGANIZATION_NAME
data["AuthUrl"] = constants.AUTHORIZER_URL + "/verify_email?token=" + token data["AuthUrl"] = constants.EnvData.AUTHORIZER_URL + "/verify_email?token=" + token
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)
@ -112,8 +112,8 @@ func SendVerificationMail(toEmail, token string) error {
// SendForgotPasswordMail to send verification email // SendForgotPasswordMail to send verification email
func SendForgotPasswordMail(toEmail, token, host string) error { func SendForgotPasswordMail(toEmail, token, host string) error {
if constants.RESET_PASSWORD_URL == "" { if constants.EnvData.RESET_PASSWORD_URL == "" {
constants.RESET_PASSWORD_URL = constants.AUTHORIZER_URL + "/app/reset-password" constants.EnvData.RESET_PASSWORD_URL = constants.EnvData.AUTHORIZER_URL + "/app/reset-password"
} }
// 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
@ -207,9 +207,9 @@ func SendForgotPasswordMail(toEmail, token, host string) error {
` `
data := make(map[string]interface{}, 3) data := make(map[string]interface{}, 3)
data["OrgLogo"] = constants.ORGANIZATION_LOGO data["OrgLogo"] = constants.EnvData.ORGANIZATION_LOGO
data["ToEmail"] = constants.ORGANIZATION_NAME data["ToEmail"] = constants.EnvData.ORGANIZATION_NAME
data["AuthUrl"] = constants.RESET_PASSWORD_URL + "?token=" + token data["AuthUrl"] = constants.EnvData.RESET_PASSWORD_URL + "?token=" + token
message = AddEmailTemplate(message, data, "reset_password_email.tmpl") message = AddEmailTemplate(message, data, "reset_password_email.tmpl")
return email.SendMail(Receiver, Subject, message) return email.SendMail(Receiver, Subject, message)

View File

@ -0,0 +1,30 @@
package utils
import (
"encoding/json"
"github.com/authorizerdev/authorizer/server/constants"
)
func EncryptConfig(data map[string]interface{}) ([]byte, error) {
jsonBytes, err := json.Marshal(data)
if err != nil {
return []byte{}, err
}
err = json.Unmarshal(jsonBytes, &constants.EnvData)
if err != nil {
return []byte{}, err
}
configData, err := json.Marshal(constants.EnvData)
if err != nil {
return []byte{}, err
}
encryptedConfig, err := EncryptAES(configData)
if err != nil {
return []byte{}, err
}
return encryptedConfig, nil
}

View File

@ -16,7 +16,7 @@ func IsValidEmail(email string) bool {
} }
func IsValidOrigin(url string) bool { func IsValidOrigin(url string) bool {
if len(constants.ALLOWED_ORIGINS) == 1 && constants.ALLOWED_ORIGINS[0] == "*" { if len(constants.EnvData.ALLOWED_ORIGINS) == 1 && constants.EnvData.ALLOWED_ORIGINS[0] == "*" {
return true return true
} }
@ -24,7 +24,7 @@ func IsValidOrigin(url string) bool {
hostName, port := GetHostParts(url) hostName, port := GetHostParts(url)
currentOrigin := hostName + ":" + port currentOrigin := hostName + ":" + port
for _, origin := range constants.ALLOWED_ORIGINS { for _, origin := range constants.EnvData.ALLOWED_ORIGINS {
replacedString := origin replacedString := origin
// if has regex whitelisted domains // if has regex whitelisted domains
if strings.Contains(origin, "*") { if strings.Contains(origin, "*") {
@ -50,12 +50,17 @@ func IsValidOrigin(url string) bool {
} }
func IsSuperAdmin(gc *gin.Context) bool { func IsSuperAdmin(gc *gin.Context) bool {
secret := gc.Request.Header.Get("x-authorizer-admin-secret") token, err := GetAdminAuthToken(gc)
if secret == "" { if err != nil {
return false secret := gc.Request.Header.Get("x-authorizer-admin-secret")
if secret == "" {
return false
}
return secret == constants.EnvData.ADMIN_SECRET
} }
return secret == constants.ADMIN_SECRET return token != ""
} }
func IsValidRoles(userRoles []string, roles []string) bool { func IsValidRoles(userRoles []string, roles []string) bool {

View File

@ -20,23 +20,23 @@ type CustomClaim struct {
} }
func CreateVerificationToken(email string, tokenType string) (string, error) { func CreateVerificationToken(email string, tokenType string) (string, error) {
t := jwt.New(jwt.GetSigningMethod(constants.JWT_TYPE)) t := jwt.New(jwt.GetSigningMethod(constants.EnvData.JWT_TYPE))
t.Claims = &CustomClaim{ t.Claims = &CustomClaim{
&jwt.StandardClaims{ &jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
}, },
tokenType, tokenType,
UserInfo{Email: email, Host: constants.AUTHORIZER_URL, RedirectURL: constants.APP_URL}, UserInfo{Email: email, Host: constants.EnvData.AUTHORIZER_URL, RedirectURL: constants.EnvData.APP_URL},
} }
return t.SignedString([]byte(constants.JWT_SECRET)) return t.SignedString([]byte(constants.EnvData.JWT_SECRET))
} }
func VerifyVerificationToken(token string) (*CustomClaim, error) { func VerifyVerificationToken(token string) (*CustomClaim, error) {
claims := &CustomClaim{} claims := &CustomClaim{}
_, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) { _, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(constants.JWT_SECRET), nil return []byte(constants.EnvData.JWT_SECRET), nil
}) })
if err != nil { if err != nil {
return claims, err return claims, err

16
templates/dashboard.tmpl Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script>
window.__authorizer__ = {{.data}}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/dashboard/build/index.js"></script>
</body>
</html>