commit
6716407321
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': 'warn',
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@ -0,0 +1,2 @@
|
||||
# Dev Notes
|
||||
should use this probably https://react-dnd.github.io/
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.3.4",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.2"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import {
|
||||
createBrowserRouter,
|
||||
createRoutesFromElements,
|
||||
Route,
|
||||
RouterProvider,
|
||||
} from 'react-router-dom'
|
||||
import Home from './Pages/Home'
|
||||
import './App.css'
|
||||
import Build from './Pages/Build'
|
||||
import Login from './Pages/Login'
|
||||
import Register from './Pages/Register'
|
||||
import SplitRegion from './Pages/Build/SplitRegion'
|
||||
import Combined from './Pages/Build/Combined'
|
||||
import NARegion from './Pages/Build/NARegion'
|
||||
import APACRegion from './Pages/Build/APACRegion'
|
||||
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route path="/" element={<Home />}>
|
||||
<Route path="build" element={<Build />}>
|
||||
<Route path="split-region" element={<SplitRegion />} />
|
||||
<Route path="combined" element={<Combined />} />
|
||||
<Route path="na" element={<NARegion />} />
|
||||
<Route path="apac" element={<APACRegion />} />
|
||||
</Route>
|
||||
<Route path='accounts'>
|
||||
<Route path='login' element={<Login />} />
|
||||
<Route path='register' element={<Register />} />
|
||||
</Route>
|
||||
</Route>
|
||||
)
|
||||
)
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterProvider router={router}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default App
|
||||
@ -0,0 +1,24 @@
|
||||
.basic-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.basic-form label {
|
||||
text-align: left;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
.basic-form input {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.basic-form .switch {
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
text-align: left;
|
||||
margin-top: 5px;
|
||||
color: #bbb;
|
||||
}
|
||||
.basic-form .switch:hover {
|
||||
color: #999;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
.basic-nav {
|
||||
color:azure;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav-right {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.nav-left {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.nav-link {
|
||||
color: aliceblue;
|
||||
}
|
||||
.nav-link:hover {
|
||||
color: grey;
|
||||
}
|
||||
.nav-header {
|
||||
text-transform:uppercase;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid white;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
.team-card {
|
||||
background-color: darkgray;
|
||||
border-radius: 6px;
|
||||
width: 260px;
|
||||
height: 48px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-align: left;
|
||||
padding: 10px 0px 0px 15px;
|
||||
margin: 0px;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
.sanfranciscoshock {
|
||||
background-color: black;
|
||||
background-image: url('../../assets/cards/sfs.png');
|
||||
color: orange;
|
||||
}
|
||||
.houstonoutlaws {
|
||||
background-color: rgb(39, 202, 69);
|
||||
color: black;
|
||||
}
|
||||
.bostonuprising {
|
||||
background-color: rgb(8, 51, 192);
|
||||
color: white;
|
||||
}
|
||||
.losangelesgladiators {
|
||||
background-color: rgb(110, 23, 223);
|
||||
color: white;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="header">
|
||||
<h1>owltier</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import './Css/Forms.css'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<div className="basic-form">
|
||||
<label htmlFor="username">username</label>
|
||||
<input type="text" name="username" id="username" />
|
||||
<label htmlFor="password">password</label>
|
||||
<input type="password" name="password" id="password" />
|
||||
<button type="submit">submit</button>
|
||||
<Link to='/accounts/register' className='switch'>dont have an account?</Link>
|
||||
<div className="form-err"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
interface MenuItem {
|
||||
link: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export default function Menu(props: {title: string, items: Array<MenuItem>, current: string, align: string}) {
|
||||
let menuitems = []
|
||||
for (let item of props.items) {
|
||||
menuitems.push(<Link to={`${item['link']}`} className='nav-link'>{item['text']}</Link>)
|
||||
}
|
||||
let align = props.align == 'left' ? 'nav-left' : 'nav-right'
|
||||
return (
|
||||
<div className={`basic-nav ${align}`}>
|
||||
<div className="nav-header">{props.title}</div>
|
||||
{menuitems}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import Menu from './Menu'
|
||||
import './Css/Menus.css'
|
||||
const Items = [
|
||||
{link: '/accounts/login', text: 'sign in / sign up'},
|
||||
{link: '/build/split-region', text: 'build tier list'},
|
||||
{link: '/community-rankings', text: 'community rankings'},
|
||||
]
|
||||
|
||||
export default function Options(props: {current: string}) {
|
||||
return (
|
||||
<Menu
|
||||
title='Navigation'
|
||||
items={Items}
|
||||
current={props.current}
|
||||
align='right'
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import './Css/Menus.css'
|
||||
import Menu from './Menu'
|
||||
|
||||
const Items = [
|
||||
{link: '/build/split-region', text: 'split region'},
|
||||
{link: '/build/combined', text: 'combined'},
|
||||
{link: '/build/na', text: 'na only'},
|
||||
{link: '/build/apac', text: 'apac only'},
|
||||
]
|
||||
|
||||
export default function Options(props: {current: string}) {
|
||||
return (
|
||||
<Menu
|
||||
title='Options'
|
||||
items={Items}
|
||||
current={props.current}
|
||||
align='left'
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import './Css/Forms.css'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function RegisterForm() {
|
||||
return (
|
||||
<div className="basic-form">
|
||||
<label htmlFor="username">username</label>
|
||||
<input type="text" name="username" id="username" />
|
||||
<label htmlFor="password">password</label>
|
||||
<input type="password" name="password" id="password" />
|
||||
<label htmlFor="confpassword">confirm password</label>
|
||||
<input type="password" name="confpassword" id="confpassword" />
|
||||
<button type="submit">submit</button>
|
||||
<Link to='/accounts/login' className='switch'>already have an account?</Link>
|
||||
<div className="form-err"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import './Css/TeamCard.css'
|
||||
import './Css/TeamStyles.css'
|
||||
|
||||
export default function TeamCard(props: {team: string}) {
|
||||
let teamCss = props.team.replaceAll(' ', '').toLowerCase()
|
||||
return (
|
||||
<div className={`team-card ${teamCss}`}>
|
||||
<h3>{props.team}</h3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import Options from '../Components/Options'
|
||||
import Nav from '../Components/Nav'
|
||||
import './Css/Main.css'
|
||||
|
||||
export default function Build() {
|
||||
return (
|
||||
<div className="page-content">
|
||||
<div className="left-menus">
|
||||
<Options current='split-region' />
|
||||
</div>
|
||||
<Outlet />
|
||||
<div className="right-menus">
|
||||
<Nav current='build' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import TeamCard from '../../Components/TeamCard'
|
||||
|
||||
export default function APACRegion() {
|
||||
return (
|
||||
<div>
|
||||
<TeamCard team="San Francisco Shock" />
|
||||
<TeamCard team="Houston Outlaws" />
|
||||
<TeamCard team="Boston Uprising" />
|
||||
<TeamCard team="Los Angeles Gladiators" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import TeamCard from '../../Components/TeamCard'
|
||||
|
||||
export default function Combined() {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
width: '600px',
|
||||
height: '700px',
|
||||
flexDirection: 'column',
|
||||
flexWrap: 'wrap',
|
||||
alignContent: 'space-between',
|
||||
}}>
|
||||
<TeamCard team="San Francisco Shock" />
|
||||
<TeamCard team="Houston Outlaws" />
|
||||
<TeamCard team="Boston Uprising" />
|
||||
<TeamCard team="Los Angeles Gladiators" />
|
||||
<TeamCard team="New York Excelsior" />
|
||||
<TeamCard team="Washington Justice" />
|
||||
<TeamCard team="Vegas Eternal" />
|
||||
<TeamCard team="Toronto Defiant" />
|
||||
<TeamCard team="Atlanta Reign" />
|
||||
<TeamCard team="Florida Mayhem" />
|
||||
<TeamCard team="London Spitfire" />
|
||||
<TeamCard team="Los Angeles Valiant" />
|
||||
<TeamCard team="Vancouver Titans" />
|
||||
<TeamCard team="Seoul Infernal" />
|
||||
<TeamCard team="Guangzhou Charge" />
|
||||
<TeamCard team="Hangzhou Spark" />
|
||||
<TeamCard team="Shanghai Dragons" />
|
||||
<TeamCard team="Dallas Fuel" />
|
||||
<TeamCard team="Seoul Dynasty" />
|
||||
<TeamCard team="Chengdu Hunters" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import TeamCard from '../../Components/TeamCard'
|
||||
|
||||
export default function NARegion() {
|
||||
return (
|
||||
<div>
|
||||
<TeamCard team="San Francisco Shock" />
|
||||
<TeamCard team="Houston Outlaws" />
|
||||
<TeamCard team="Boston Uprising" />
|
||||
<TeamCard team="Los Angeles Gladiators" />
|
||||
<TeamCard team="New York Excelsior" />
|
||||
<TeamCard team="Washington Justice" />
|
||||
<TeamCard team="Vegas Eternal" />
|
||||
<TeamCard team="Toronto Defiant" />
|
||||
<TeamCard team="Atlanta Reign" />
|
||||
<TeamCard team="Florida Mayhem" />
|
||||
<TeamCard team="London Spitfire" />
|
||||
<TeamCard team="Los Angeles Valiant" />
|
||||
<TeamCard team="Vancouver Titans" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import TeamCard from '../../Components/TeamCard'
|
||||
|
||||
export default function SplitRegion() {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
width: '600px',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<div className="na">
|
||||
<TeamCard team="San Francisco Shock" />
|
||||
<TeamCard team="Houston Outlaws" />
|
||||
<TeamCard team="Boston Uprising" />
|
||||
<TeamCard team="Los Angeles Gladiators" />
|
||||
<TeamCard team="New York Excelsior" />
|
||||
<TeamCard team="Washington Justice" />
|
||||
<TeamCard team="Vegas Eternal" />
|
||||
<TeamCard team="Toronto Defiant" />
|
||||
<TeamCard team="Atlanta Reign" />
|
||||
<TeamCard team="Florida Mayhem" />
|
||||
<TeamCard team="London Spitfire" />
|
||||
<TeamCard team="Los Angeles Valiant" />
|
||||
<TeamCard team="Vancouver Titans" />
|
||||
</div>
|
||||
<div className="apac">
|
||||
<TeamCard team="Seoul Infernal" />
|
||||
<TeamCard team="Guangzhou Charge" />
|
||||
<TeamCard team="Hangzhou Spark" />
|
||||
<TeamCard team="Shanghai Dragons" />
|
||||
<TeamCard team="Dallas Fuel" />
|
||||
<TeamCard team="Seoul Dynasty" />
|
||||
<TeamCard team="Chengdu Hunters" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
.page-content {
|
||||
width: 960px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.no-w {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.full-container {
|
||||
height: 900px;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import Header from '../Components/Header'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className='full-container'>
|
||||
<Header />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import LoginForm from '../Components/LoginForm'
|
||||
import Nav from '../Components/Nav'
|
||||
|
||||
export default function Login() {
|
||||
return (
|
||||
<div className="page-content no-w">
|
||||
<LoginForm />
|
||||
<Nav current='login'/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import RegisterForm from '../Components/RegisterForm'
|
||||
import Nav from '../Components/Nav'
|
||||
|
||||
export default function Login() {
|
||||
return (
|
||||
<div className="page-content no-w">
|
||||
<RegisterForm />
|
||||
<Nav current='login'/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,68 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.1s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #ffffff;
|
||||
}
|
||||
button:active {
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@ -0,0 +1,37 @@
|
||||
module github.com/mnrva-dev/owltier.com
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.24
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.25
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.7
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
)
|
||||
@ -0,0 +1,57 @@
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.24 h1:G0mJzpMjJFtK+7KtAky2kAjio21BdzNXblQSm2ZKsy0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.24/go.mod h1:+9/RIaxGG2let2y9lIYEwOTBhaXqArOakom2TVytvFE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.23 h1:uKTIH4RmFIo04Pijn132WEMaboVLAg96H4l2KFRGzZU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.23/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.25 h1:/+Z/dCO+1QHOlCm7m9G61snvIaDRUTv/HXp+8HdESiY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.25/go.mod h1:JQ0HJ+3LaAKHx3uwRUAfR/tb/gOlgAGPT6mZfIq55Ec=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.7 h1:yb2o8oh3Y+Gg2g+wlzrWS3pB89+dHrXayT/d9cs8McU=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.7/go.mod h1:1MNss6sqoIsFGisX92do/5doiUCBrN7EjhZCS/8DUjI=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.11 h1:WHi9VKMYGtWt2DzqeYHXzt55aflymO2EZ6axuKla8oU=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.11/go.mod h1:pP+91QTpJMvcFTqGky6puHrkBs8oqoB3XOCiGRDaXwI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27 h1:QmyPCRZNMR1pFbiOi9kBZWZuKrKB9LD4cxltxQk4tNE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27/go.mod h1:DfuVY36ixXnsG+uTqnoLWunXAKJ4qjccoFrXUPpj+hs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
|
||||
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
||||
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/mnrva-dev/owltier.com/server"
|
||||
|
||||
func main() {
|
||||
server.Run()
|
||||
}
|
||||
@ -0,0 +1,248 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mnrva-dev/owltier.com/server/auth"
|
||||
"github.com/mnrva-dev/owltier.com/server/config"
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
"github.com/mnrva-dev/owltier.com/server/token"
|
||||
)
|
||||
|
||||
// TODO also write unit tests
|
||||
|
||||
type userdata struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var (
|
||||
testuser = &db.UserSchema{
|
||||
Username: "test",
|
||||
Password: "testpassword1234!!",
|
||||
Email: "test@example.com",
|
||||
}
|
||||
permatestuser = &db.UserSchema{
|
||||
Id: "0a85b0ae-a577-4be3-8609-d443d50f6939",
|
||||
Username: "user",
|
||||
Password: "password1234!!",
|
||||
Email: "user@example.com",
|
||||
Refresh: "",
|
||||
}
|
||||
)
|
||||
|
||||
func runTestServer() *httptest.Server {
|
||||
return httptest.NewServer(auth.BuildRouter())
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
ts := runTestServer()
|
||||
defer ts.Close()
|
||||
data := url.Values{}
|
||||
data.Set("email", testuser.Email)
|
||||
if config.UsernamesEnabled() {
|
||||
data.Set("username", testuser.Username)
|
||||
}
|
||||
data.Set("password", testuser.Password)
|
||||
resp, err := http.PostForm(fmt.Sprintf("%s/register", ts.URL), data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read body")
|
||||
}
|
||||
if resp.StatusCode/100 != 2 {
|
||||
t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode)
|
||||
fmt.Println(string(body))
|
||||
t.FailNow()
|
||||
}
|
||||
AccessToken := strings.Split(string(body), "\n")[0]
|
||||
RefreshToken := strings.Split(string(body), "\n")[1]
|
||||
if AccessToken == "" || RefreshToken == "" {
|
||||
t.Errorf("Expected access and refresh token to be set")
|
||||
}
|
||||
if AccessToken == RefreshToken {
|
||||
t.Errorf("Expected access and refresh tokens to be different")
|
||||
}
|
||||
_, err = token.ValidateAccess(AccessToken)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid access token, got %v", err)
|
||||
}
|
||||
_, err = token.ValidateRefresh(RefreshToken)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid refresh token, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
ts := runTestServer()
|
||||
defer ts.Close()
|
||||
data := url.Values{}
|
||||
data.Set("email", testuser.Email)
|
||||
if config.UsernamesEnabled() {
|
||||
data.Set("username", testuser.Username)
|
||||
}
|
||||
data.Set("password", testuser.Password)
|
||||
resp, err := http.PostForm(fmt.Sprintf("%s/login", ts.URL), data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read body")
|
||||
}
|
||||
if resp.StatusCode/100 != 2 {
|
||||
t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode)
|
||||
fmt.Println(string(body))
|
||||
t.FailNow()
|
||||
}
|
||||
AccessToken := strings.Split(string(body), "\n")[0]
|
||||
RefreshToken := strings.Split(string(body), "\n")[1]
|
||||
if AccessToken == "" || RefreshToken == "" {
|
||||
t.Errorf("Expected access and refresh token to be set")
|
||||
}
|
||||
if AccessToken == RefreshToken {
|
||||
t.Errorf("Expected access and refresh tokens to be different")
|
||||
}
|
||||
_, err = token.ValidateAccess(AccessToken)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid access token, got %v", err)
|
||||
}
|
||||
_, err = token.ValidateRefresh(RefreshToken)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid refresh token, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
ts := runTestServer()
|
||||
defer ts.Close()
|
||||
data := url.Values{}
|
||||
data.Set("email", testuser.Email)
|
||||
data.Set("password", testuser.Password)
|
||||
resp, err := http.PostForm(fmt.Sprintf("%s/login", ts.URL), data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read body")
|
||||
}
|
||||
if resp.StatusCode/100 != 2 {
|
||||
t.Error("Failed to login")
|
||||
t.FailNow()
|
||||
}
|
||||
RefreshToken := strings.Split(string(body), "\n")[1]
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/token/refresh", ts.URL), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't create request: %v", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+RefreshToken)
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read body")
|
||||
}
|
||||
if resp.StatusCode/100 != 2 {
|
||||
t.Errorf("%s\n", string(body))
|
||||
t.Fatalf("Expected token to be validated but got status %d", resp.StatusCode)
|
||||
}
|
||||
AccessToken := strings.Split(string(body), "\n")[0]
|
||||
RefreshToken = strings.Split(string(body), "\n")[1]
|
||||
if AccessToken == "" || RefreshToken == "" {
|
||||
t.Errorf("Expected access and refresh token to be set")
|
||||
}
|
||||
if AccessToken == RefreshToken {
|
||||
t.Errorf("Expected access and refresh tokens to be different")
|
||||
}
|
||||
_, err = token.ValidateAccess(AccessToken)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid access token, got %v", err)
|
||||
}
|
||||
_, err = token.ValidateRefresh(RefreshToken)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid refresh token, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAccess(t *testing.T) {
|
||||
ts := runTestServer()
|
||||
defer ts.Close()
|
||||
AccessToken := token.GenerateAccess(testuser)
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/token/validate", ts.URL), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't create request: %v", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+AccessToken)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read body")
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(string(body))
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode/100 != 2 {
|
||||
t.Error(string(body))
|
||||
t.Errorf("Expected token to be validated but got status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAccount(t *testing.T) {
|
||||
ts := runTestServer()
|
||||
defer ts.Close()
|
||||
data := url.Values{}
|
||||
data.Set("email", testuser.Email)
|
||||
data.Set("password", testuser.Password)
|
||||
resp, err := http.PostForm(fmt.Sprintf("%s/login", ts.URL), data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read body")
|
||||
}
|
||||
if resp.StatusCode/100 != 2 {
|
||||
t.Error("Failed to login")
|
||||
t.FailNow()
|
||||
}
|
||||
AccessToken := strings.Split(string(body), "\n")[0]
|
||||
data = url.Values{}
|
||||
data.Set("password", testuser.Password)
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/delete", ts.URL), strings.NewReader(data.Encode()))
|
||||
req.Header.Add("Authorization", "Bearer "+AccessToken)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read body")
|
||||
}
|
||||
// fmt.Println(string(body))
|
||||
if resp.StatusCode/100 != 2 {
|
||||
t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode)
|
||||
fmt.Println(string(body))
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
package auth_test
|
||||
@ -0,0 +1,40 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
"github.com/mnrva-dev/owltier.com/server/jsend"
|
||||
"github.com/mnrva-dev/owltier.com/server/middleware"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Need login details AND valid token to delete an account
|
||||
func DeleteAccount(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
password := strings.TrimSpace(r.FormValue("password"))
|
||||
|
||||
// get user from token parse middleware
|
||||
user, err := r.Context().Value(middleware.ContextKeyValues).(*middleware.Values).GetUser()
|
||||
if err != nil {
|
||||
jsend.Error(w, "Failed to retrieve user information")
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||
if err != nil {
|
||||
jsend.Fail(w, 401, map[string]interface{}{
|
||||
"password": "Password is incorrect"})
|
||||
return
|
||||
}
|
||||
|
||||
// at this point, password is correct and token is valid
|
||||
|
||||
err = db.Delete(user)
|
||||
if err != nil {
|
||||
jsend.Error(w, "Failed to delete user")
|
||||
return
|
||||
}
|
||||
jsend.Success(w, nil)
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mnrva-dev/owltier.com/server/config"
|
||||
passwordvalidator "github.com/wagslane/go-password-validator"
|
||||
)
|
||||
|
||||
const (
|
||||
minPasswordEntropy = 60
|
||||
)
|
||||
|
||||
type RequestForm struct {
|
||||
Username string
|
||||
Password string
|
||||
Email string
|
||||
Redirect bool
|
||||
RedirectUrl string
|
||||
}
|
||||
|
||||
func (h *RequestForm) validate() error {
|
||||
if h.Username == "" && config.UsernamesEnabled() {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
if h.Password == "" {
|
||||
return errors.New("password is required")
|
||||
}
|
||||
if h.Email == "" {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
if _, err := mail.ParseAddress(h.Email); err != nil {
|
||||
return errors.New("email address is not valid")
|
||||
}
|
||||
if h.Redirect {
|
||||
if h.RedirectUrl == "" {
|
||||
return errors.New("redirect url is required")
|
||||
}
|
||||
var url []byte
|
||||
_, err := base64.NewDecoder(base64.StdEncoding, strings.NewReader(h.RedirectUrl)).Read(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.RedirectUrl = string(url)
|
||||
}
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9-_]{3,24}$`).MatchString(h.Username) && config.UsernamesEnabled() {
|
||||
return errors.New("username is not valid")
|
||||
}
|
||||
return passwordvalidator.Validate(h.Password, minPasswordEntropy)
|
||||
}
|
||||
|
||||
func (h *RequestForm) Parse(r *http.Request) error {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.UsernamesEnabled() {
|
||||
h.Username = strings.TrimSpace(r.FormValue("username"))
|
||||
}
|
||||
h.Password = strings.TrimSpace(r.FormValue("password"))
|
||||
// truncate extremely long passwords
|
||||
if len(h.Password) > 128 {
|
||||
h.Password = h.Password[:128]
|
||||
}
|
||||
h.Email = strings.TrimSpace(r.FormValue("email"))
|
||||
h.Redirect = r.FormValue("redirect") != "" || strings.ToLower(r.FormValue("redirect")) == "false"
|
||||
h.RedirectUrl = strings.TrimSpace(r.FormValue("redirect_url"))
|
||||
|
||||
return h.validate()
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
"github.com/mnrva-dev/owltier.com/server/jsend"
|
||||
"github.com/mnrva-dev/owltier.com/server/token"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
var form = &RequestForm{}
|
||||
if err := form.Parse(r); err != nil {
|
||||
jsend.Error(w, "Failed to parse form body")
|
||||
return
|
||||
}
|
||||
|
||||
// get user from DB
|
||||
var user = &db.UserSchema{}
|
||||
err := db.FetchByGsi(&db.UserSchema{
|
||||
Email: form.Email,
|
||||
}, user)
|
||||
if err != nil {
|
||||
jsend.Fail(w, 401, map[string]interface{}{
|
||||
"message": "Email or password is invalid"})
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.Password))
|
||||
if err != nil {
|
||||
jsend.Fail(w, 401, map[string]interface{}{
|
||||
"message": "Email or password is invalid"})
|
||||
return
|
||||
}
|
||||
|
||||
// prepare login information for the client
|
||||
accessT := token.GenerateAccess(user)
|
||||
refreshT := token.GenerateRefresh(user)
|
||||
db.Update(user, "refresh_token", refreshT)
|
||||
db.Update(user, "last_login_at", time.Now().Unix())
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "_owltier.com_auth",
|
||||
Value: accessT,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "_owltier.com_refresh",
|
||||
Value: accessT,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
})
|
||||
jsend.Success(w, map[string]interface{}{
|
||||
"id": user.Id,
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
"github.com/mnrva-dev/owltier.com/server/jsend"
|
||||
"github.com/mnrva-dev/owltier.com/server/token"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func Register(w http.ResponseWriter, r *http.Request) {
|
||||
var form = &RequestForm{}
|
||||
if err := form.Parse(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get user from DB
|
||||
var user = &db.UserSchema{}
|
||||
err := db.FetchByGsi(&db.UserSchema{
|
||||
Email: form.Email,
|
||||
}, user)
|
||||
// if we didnt get NotFound error...
|
||||
if err == nil {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
w.Write([]byte("user already exists"))
|
||||
return
|
||||
} // TODO There is probably a better way to make sure this is just a
|
||||
// "Not Found" error and not an actual error
|
||||
|
||||
user.CreatedAt = time.Now().Unix()
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.Password = string(hashedPassword)
|
||||
user.Email = form.Email
|
||||
user.Id = uuid.New().String()
|
||||
user.Scope = "default"
|
||||
user.EmailIsVerified = false
|
||||
|
||||
accessT := token.GenerateAccess(user)
|
||||
refreshT := token.GenerateRefresh(user)
|
||||
if accessT == "" || refreshT == "" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
user.Refresh = refreshT
|
||||
|
||||
err = db.Create(user)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "_owltier_auth",
|
||||
Value: accessT,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "_owltier_refresh",
|
||||
Value: accessT,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
})
|
||||
if form.Redirect {
|
||||
http.Redirect(w, r, form.RedirectUrl, 303)
|
||||
} else {
|
||||
jsend.Success(w, map[string]interface{}{
|
||||
"id": user.Id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/mnrva-dev/owltier.com/server/middleware"
|
||||
)
|
||||
|
||||
func BuildRouter() *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Post("/login", Login)
|
||||
r.Post("/register", Register)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.TokenValidater)
|
||||
r.Post("/delete", DeleteAccount)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.RefreshValidator)
|
||||
r.Post("/token/refresh", Refresh)
|
||||
})
|
||||
r.Get("/token/validate", Validate)
|
||||
|
||||
return r
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
"github.com/mnrva-dev/owltier.com/server/jsend"
|
||||
"github.com/mnrva-dev/owltier.com/server/middleware"
|
||||
"github.com/mnrva-dev/owltier.com/server/token"
|
||||
)
|
||||
|
||||
func Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// get user and token from token parse middleware
|
||||
user, err := r.Context().Value(middleware.ContextKeyValues).(*middleware.Values).GetUser()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
t, err := r.Context().Value(middleware.ContextKeyValues).(*middleware.Values).GetRefreshToken()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if user.Refresh != t {
|
||||
http.Error(w, "Token mismatch", http.StatusUnauthorized)
|
||||
fmt.Printf("%s\n%s", user.Refresh, t)
|
||||
return
|
||||
}
|
||||
|
||||
// prepare login information for the client
|
||||
accessT := token.GenerateAccess(user)
|
||||
refreshT := token.GenerateRefresh(user)
|
||||
db.Update(user, "RefreshToken", refreshT)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "_owltier.com_auth",
|
||||
Value: accessT,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "_owltier.com_refresh",
|
||||
Value: accessT,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
})
|
||||
jsend.Success(w, nil)
|
||||
}
|
||||
|
||||
func Validate(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
headerVals := strings.Split(header, " ")
|
||||
if strings.ToLower(headerVals[0]) != "bearer" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprint(w, "Bad Authorization Scheme")
|
||||
}
|
||||
t := headerVals[1]
|
||||
if t == "" {
|
||||
w.WriteHeader(400)
|
||||
fmt.Fprint(w, "No Token Provided")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := token.ValidateAccess(t)
|
||||
if err != nil {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Type != "Access" {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
return
|
||||
}
|
||||
jsend.Success(w, nil)
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.Println("* Loading Environment Configuration")
|
||||
loadEnv()
|
||||
}
|
||||
|
||||
func loadEnv() {
|
||||
projectName := regexp.MustCompile(`^(.*` + "owltier.com" + `)`)
|
||||
currentWorkDirectory, _ := os.Getwd()
|
||||
rootPath := projectName.Find([]byte(currentWorkDirectory))
|
||||
|
||||
err := godotenv.Load(string(rootPath) + `/.env.local`)
|
||||
|
||||
if err != nil {
|
||||
log.Println("* Error loading .env file")
|
||||
}
|
||||
}
|
||||
|
||||
func Environment() string {
|
||||
return os.Getenv("ENVIRONMENT")
|
||||
}
|
||||
|
||||
func ListenAddr() string {
|
||||
return os.Getenv("LISTEN_ADDR")
|
||||
}
|
||||
|
||||
func AccessSecret() []byte {
|
||||
return []byte(os.Getenv("JWT_ACCESS_SECRET"))
|
||||
}
|
||||
|
||||
func RefreshSecret() []byte {
|
||||
return []byte(os.Getenv("JWT_REFRESH_SECRET"))
|
||||
}
|
||||
|
||||
func EmailTokenSecret() []byte {
|
||||
return []byte(os.Getenv("JWT_EMAIL_SECRET"))
|
||||
}
|
||||
|
||||
func JwtIssuer() string {
|
||||
return os.Getenv("JWT_ISSUER")
|
||||
}
|
||||
|
||||
func JwtAudience() []string {
|
||||
aud := os.Getenv("JWT_AUDIENCE")
|
||||
audience := strings.Split(aud, ",")
|
||||
return audience
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
func DbUrl() string {
|
||||
return os.Getenv("DB_URL")
|
||||
}
|
||||
|
||||
func DbTable() string {
|
||||
return os.Getenv("DB_TABLE")
|
||||
}
|
||||
|
||||
func DbGsiName() string {
|
||||
return os.Getenv("DB_GSI_NAME")
|
||||
}
|
||||
|
||||
func DbGsiAttr() string {
|
||||
return os.Getenv("DB_GSI_ATTR")
|
||||
}
|
||||
|
||||
func UsernamesEnabled() bool {
|
||||
if os.Getenv("CFG_USERNAMES_ENABLED") == "true" {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
func GoogleAccessToken() string {
|
||||
return os.Getenv("GOOGLE_ACCESS_TOKEN")
|
||||
}
|
||||
|
||||
func GoogleRefreshToken() string {
|
||||
return os.Getenv("GOOGLE_REFRESH_TOKEN")
|
||||
}
|
||||
|
||||
func GoogleClientID() string {
|
||||
return os.Getenv("GOOGLE_CLIENT_ID")
|
||||
}
|
||||
|
||||
func GoogleClientSecret() string {
|
||||
return os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
}
|
||||
|
||||
func AmazonSmtpUsername() string {
|
||||
return os.Getenv("AMZN_SMTP_USERNAME")
|
||||
}
|
||||
|
||||
func AmazonSmtpPassword() string {
|
||||
return os.Getenv("AMZN_SMTP_PASSWORD")
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
)
|
||||
|
||||
func Create(i DbItem) error {
|
||||
|
||||
i.buildKeys()
|
||||
|
||||
av, err := attributevalue.MarshalMap(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = handle.client.PutItem(context.TODO(), &dynamodb.PutItemInput{
|
||||
TableName: aws.String(handle.table),
|
||||
Item: av,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// out must be a non-nil pointer
|
||||
func Fetch(i DbItem, out interface{}) error {
|
||||
|
||||
i.buildKeys()
|
||||
|
||||
o, err := handle.client.GetItem(context.TODO(), &dynamodb.GetItemInput{
|
||||
TableName: aws.String(handle.table),
|
||||
Key: i.getKey(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if o.Item == nil {
|
||||
return NotFoundError(errors.New("item not found"))
|
||||
}
|
||||
|
||||
err = attributevalue.UnmarshalMap(o.Item, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func FetchByGsi(i DbItem, out interface{}) error {
|
||||
|
||||
i.buildKeys()
|
||||
|
||||
o, err := handle.client.Query(context.TODO(), &dynamodb.QueryInput{
|
||||
TableName: aws.String(handle.table),
|
||||
IndexName: aws.String(handle.gsiName),
|
||||
KeyConditionExpression: aws.String(fmt.Sprintf("%s = :%s", handle.gsiAttr, handle.gsiAttr)),
|
||||
ExpressionAttributeValues: i.getGsi(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if o.Count > 1 {
|
||||
return MultipleItemsError(errors.New("multiple items found"))
|
||||
} else if o.Count < 1 {
|
||||
return NotFoundError(errors.New("item not found"))
|
||||
}
|
||||
|
||||
err = attributevalue.UnmarshalMap(o.Items[0], out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Update(i DbItem, key string, value interface{}) error {
|
||||
val, err := attributevalue.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = handle.client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
|
||||
TableName: aws.String(handle.table),
|
||||
Key: i.getKey(),
|
||||
UpdateExpression: aws.String(fmt.Sprintf("set %s = :val", key)),
|
||||
ExpressionAttributeValues: map[string]types.AttributeValue{
|
||||
":val": val,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MultiUpdate(i DbItem, keys []string, values []interface{}) error {
|
||||
// TODO implement this
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func Delete(i DbItem) error {
|
||||
|
||||
k := i.getKey()
|
||||
|
||||
_, err := handle.client.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{
|
||||
TableName: aws.String(handle.table),
|
||||
Key: k,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
)
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
u := &db.UserSchema{
|
||||
Username: "myuser",
|
||||
Email: "me@example.com",
|
||||
Password: "secret123",
|
||||
}
|
||||
|
||||
err := db.Create(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
u := &db.UserSchema{
|
||||
Username: "myuser",
|
||||
}
|
||||
err := db.Fetch(u, u)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if u.Email != "me@example.com" {
|
||||
t.Fatalf("Expected correct email, got %v", u.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGsiRead(t *testing.T) {
|
||||
u := &db.UserSchema{
|
||||
Email: "me@example.com",
|
||||
}
|
||||
err := db.FetchByGsi(u, u)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if u.Username != "myuser" {
|
||||
t.Fatalf("Expected correct username, got %v", u.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
u := &db.UserSchema{
|
||||
Username: "myuser",
|
||||
Email: "me@example.com",
|
||||
Password: "secret123",
|
||||
}
|
||||
err := db.Delete(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
err = db.Fetch(u, u)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected item to be deleted, but item exists")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"github.com/mnrva-dev/owltier.com/server/config"
|
||||
)
|
||||
|
||||
var (
|
||||
handle Handle
|
||||
)
|
||||
|
||||
const (
|
||||
GSI_NAME = "gsi1"
|
||||
GSI_ATTR = "gsi1pk"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
client *dynamodb.Client
|
||||
table string
|
||||
gsiName string
|
||||
gsiAttr string
|
||||
}
|
||||
|
||||
func init() {
|
||||
handle = DBConfig(
|
||||
config.Environment(),
|
||||
GSI_NAME,
|
||||
GSI_ATTR,
|
||||
)
|
||||
}
|
||||
|
||||
func CreateLocalClient() *dynamodb.Client {
|
||||
cfg, err := awsconfig.LoadDefaultConfig(context.TODO(),
|
||||
awsconfig.WithRegion("us-east-1"),
|
||||
awsconfig.WithEndpointResolver(aws.EndpointResolverFunc(
|
||||
func(service, region string) (aws.Endpoint, error) {
|
||||
return aws.Endpoint{URL: "http://localhost:8000"}, nil
|
||||
})),
|
||||
awsconfig.WithCredentialsProvider(credentials.StaticCredentialsProvider{
|
||||
Value: aws.Credentials{
|
||||
AccessKeyID: "dummy", SecretAccessKey: "dummy", SessionToken: "dummy",
|
||||
Source: "Hard-coded credentials; values are irrelevant for local DynamoDB",
|
||||
},
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return dynamodb.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
func DBConfig(env, gsiname, gsiattr string) Handle {
|
||||
var c *dynamodb.Client
|
||||
|
||||
if env == "local" {
|
||||
c = CreateLocalClient()
|
||||
log.Println("* Local Environment Detected")
|
||||
}
|
||||
return Handle{
|
||||
client: c,
|
||||
table: config.DbTable(),
|
||||
gsiName: gsiname,
|
||||
gsiAttr: gsiattr,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
)
|
||||
|
||||
type DbItem interface {
|
||||
buildKeys()
|
||||
getKey() map[string]types.AttributeValue
|
||||
getGsi() map[string]types.AttributeValue
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package db
|
||||
|
||||
type NotFoundError error
|
||||
type MultipleItemsError error
|
||||
@ -0,0 +1,39 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
)
|
||||
|
||||
type UserSchema struct {
|
||||
Pk string `dynamodbav:"pk"`
|
||||
Gsi1pk string `dynamodbav:"gsi1pk"`
|
||||
Id string `dynamodbav:"id"`
|
||||
Username string `dynamodbav:"username,omitempty"`
|
||||
Email string `dynamodbav:"email"`
|
||||
EmailIsVerified bool `dynamodbav:"email_is_verified"`
|
||||
Password string `dynamodbav:"password"`
|
||||
Refresh string `dynamodbav:"refresh_token"`
|
||||
Scope string `dynamodbav:"scope"`
|
||||
Policies []string `dynamodbav:"policies"`
|
||||
CreatedAt int64 `dynamodbav:"created_at"`
|
||||
LastLoginAt int64 `dynamodbav:"last_login_at"`
|
||||
}
|
||||
|
||||
func (u *UserSchema) buildKeys() {
|
||||
u.Pk = "user#" + u.Id
|
||||
u.Gsi1pk = "email#" + u.Email
|
||||
}
|
||||
|
||||
func (u *UserSchema) getKey() map[string]types.AttributeValue {
|
||||
u.buildKeys()
|
||||
k := make(map[string]types.AttributeValue)
|
||||
k["pk"] = &types.AttributeValueMemberS{Value: u.Pk}
|
||||
return k
|
||||
}
|
||||
|
||||
func (u *UserSchema) getGsi() map[string]types.AttributeValue {
|
||||
u.buildKeys()
|
||||
k := make(map[string]types.AttributeValue)
|
||||
k[":gsi1pk"] = &types.AttributeValueMemberS{Value: u.Gsi1pk}
|
||||
return k
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package jsend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Error(w http.ResponseWriter, message string) {
|
||||
w.WriteHeader(500)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("{\"status\": \"error\", \"message\": " + string(message) + "}"))
|
||||
}
|
||||
|
||||
func ErrorWithCode(w http.ResponseWriter, code int, message string) {
|
||||
w.WriteHeader(500)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("{\"status\": \"error\", \"message\": " + string(message) + ", \"code\":" + strconv.Itoa(code) + "}"))
|
||||
}
|
||||
|
||||
func ErrorWithData(w http.ResponseWriter, message string, data map[string]interface{}) {
|
||||
jdata, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
jdata = []byte("null")
|
||||
}
|
||||
w.WriteHeader(500)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("{\"status\": \"error\", \"message\": " + string(message) + ", \"data\":" + string(jdata) + "}"))
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package jsend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Fail(w http.ResponseWriter, code int, data map[string]interface{}) {
|
||||
jdata, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
jdata = []byte("null")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
w.Write([]byte("{\"status\": \"fail\", \"data\": " + string(jdata) + "}"))
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// JSend is a package to make standardizing API responses easy
|
||||
// It is based on the jsend specification that can be found at:
|
||||
// https://github.com/omniti-labs/jsend
|
||||
package jsend
|
||||
|
||||
type response struct {
|
||||
// Required. Either "success" for a successful request, "fail"
|
||||
// for a failed request, or "error" for a server error.
|
||||
Status string `json:"status"`
|
||||
// Required for "success" and "fail". Data is a wrapper for
|
||||
// any information the API returns.
|
||||
// If the reasons for failure correspond to POST values,
|
||||
// the response object's keys SHOULD correspond to those POST values.
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
// Required for "error". A meaningful, end-user-readable
|
||||
// message, explaining what went wrong.
|
||||
Message string `json:"message,omitempty"`
|
||||
// Optional for "error". A numerical error code.
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package jsend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// A response denoting a successful request.
|
||||
// More information about a success response can be found at:
|
||||
// https://github.com/omniti-labs/jsend
|
||||
func Success(w http.ResponseWriter, data map[string]interface{}) {
|
||||
jdata, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
jdata = []byte("null")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{\"status\": \"success\", \"data\": " + string(jdata) + "}"))
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package middleware
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
ContextKeyUser = ContextKey("user")
|
||||
ContextKeyToken = ContextKey("token")
|
||||
ContextKeyValues = ContextKey("values")
|
||||
)
|
||||
@ -0,0 +1,138 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
"github.com/mnrva-dev/owltier.com/server/token"
|
||||
)
|
||||
|
||||
type Values struct {
|
||||
m map[string]interface{}
|
||||
}
|
||||
|
||||
func (v Values) GetUser() (*db.UserSchema, error) {
|
||||
u, ok := v.m["user"].(*db.UserSchema)
|
||||
if !ok || u == nil {
|
||||
return nil, errors.New("user is not set")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
func (v Values) GetAccessToken() (string, error) {
|
||||
u, ok := v.m["access"].(string)
|
||||
if !ok {
|
||||
return "", errors.New("access token is not set")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
func (v Values) GetRefreshToken() (string, error) {
|
||||
u, ok := v.m["refresh"].(string)
|
||||
if !ok {
|
||||
return "", errors.New("refresh token is not set")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func TokenValidater(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
headerVals := strings.Split(header, " ")
|
||||
if strings.ToLower(headerVals[0]) != "bearer" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprint(w, "Bad Authorization Scheme")
|
||||
}
|
||||
t := headerVals[1]
|
||||
if t == "" {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "No Token Provided")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := token.ValidateAccess(t)
|
||||
if err != nil {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Type != token.TypeAccess {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var user = &db.UserSchema{}
|
||||
err = db.Fetch(&db.UserSchema{Id: claims.Id}, user)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(400)
|
||||
fmt.Fprint(w, "User Not Found With Id: "+claims.Id)
|
||||
return
|
||||
}
|
||||
|
||||
v := Values{map[string]interface{}{
|
||||
"user": user,
|
||||
"access": t,
|
||||
}}
|
||||
|
||||
ctx := context.WithValue(r.Context(), ContextKeyValues, &v)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func RefreshValidator(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
headerVals := strings.Split(header, " ")
|
||||
if strings.ToLower(headerVals[0]) != "bearer" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprint(w, "Bad Authorization Scheme")
|
||||
}
|
||||
t := headerVals[1]
|
||||
if t == "" {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "No Token Provided")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := token.ValidateRefresh(t)
|
||||
if err != nil {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Type != token.TypeRefresh {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var user = &db.UserSchema{}
|
||||
err = db.Fetch(&db.UserSchema{Id: claims.Id}, user)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
fmt.Fprint(w, "User Not Found")
|
||||
return
|
||||
}
|
||||
|
||||
v := Values{map[string]interface{}{
|
||||
"user": user,
|
||||
"refresh": t,
|
||||
}}
|
||||
|
||||
ctx := context.WithValue(r.Context(), ContextKeyValues, &v)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/mnrva-dev/owltier.com/server/auth"
|
||||
"github.com/mnrva-dev/owltier.com/server/config"
|
||||
)
|
||||
|
||||
// boilerplate taken from go-chi on GitHub
|
||||
func Run() {
|
||||
// The HTTP Server
|
||||
server := &http.Server{Addr: config.ListenAddr(), Handler: handler()}
|
||||
|
||||
// Server run context
|
||||
serverCtx, serverStopCtx := context.WithCancel(context.Background())
|
||||
|
||||
// Listen for syscall signals for process to interrupt/quit
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
go func() {
|
||||
<-sig
|
||||
|
||||
log.Println("* Server interrupted, shutting down...")
|
||||
// Shutdown signal with grace period of 30 seconds
|
||||
shutdownCtx, timeoutCtx := context.WithTimeout(serverCtx, 30*time.Second)
|
||||
|
||||
go func() {
|
||||
<-shutdownCtx.Done()
|
||||
if shutdownCtx.Err() == context.DeadlineExceeded {
|
||||
log.Fatal("* Graceful shutdown timed out, forcing exit...")
|
||||
}
|
||||
}()
|
||||
|
||||
/*
|
||||
* Graceful shutdown logic goes here
|
||||
*/
|
||||
|
||||
// Trigger graceful shutdown
|
||||
err := server.Shutdown(shutdownCtx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
timeoutCtx()
|
||||
serverStopCtx()
|
||||
}()
|
||||
|
||||
// Run the server
|
||||
log.Println("* Listening on http://" + config.ListenAddr())
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for server context to be stopped
|
||||
<-serverCtx.Done()
|
||||
|
||||
log.Println("* Shutdown complete.")
|
||||
}
|
||||
|
||||
func handler() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
r.Mount("/auth", auth.BuildRouter())
|
||||
|
||||
return r
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mnrva-dev/owltier.com/server/config"
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
Id string `json:"id"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email"`
|
||||
EmailIsVerified bool `json:"email_verified"`
|
||||
XSRF string `json:"xsrf"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Policies []string `json:"policies,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
func BuildClaims(user *db.UserSchema) *Claims {
|
||||
var c = &Claims{
|
||||
Id: user.Id,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
EmailIsVerified: user.EmailIsVerified,
|
||||
XSRF: uuid.New().String(),
|
||||
Scope: user.Scope,
|
||||
Policies: user.Policies,
|
||||
}
|
||||
c.IssuedAt = jwt.NewNumericDate(time.Now())
|
||||
c.NotBefore = jwt.NewNumericDate(time.Now())
|
||||
c.Issuer = config.JwtIssuer()
|
||||
c.Audience = config.JwtAudience()
|
||||
if c.Scope == "" {
|
||||
c.Scope = "default"
|
||||
}
|
||||
return c
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/mnrva-dev/owltier.com/server/config"
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
)
|
||||
|
||||
const (
|
||||
ACCESS_EXPIRATION = 10 * time.Minute
|
||||
REFRESH_EXPIRATION = 7 * 24 * time.Hour
|
||||
VERIFY_EMAIL_EXPIRATION = 15 * time.Minute
|
||||
)
|
||||
|
||||
func GenerateAccess(user *db.UserSchema) string {
|
||||
c := BuildClaims(user)
|
||||
c.ExpiresAt = jwt.NewNumericDate(time.Now().Add(ACCESS_EXPIRATION))
|
||||
c.Type = TypeAccess
|
||||
c.Id = user.Id
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
|
||||
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err := token.SignedString(config.AccessSecret())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func GenerateRefresh(user *db.UserSchema) string {
|
||||
c := BuildClaims(user)
|
||||
c.ExpiresAt = jwt.NewNumericDate(time.Now().Add(REFRESH_EXPIRATION))
|
||||
c.Type = TypeRefresh
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
|
||||
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err := token.SignedString(config.RefreshSecret())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func GenerateVerifyEmail(user *db.UserSchema) string {
|
||||
c := &Claims{}
|
||||
c.Id = user.Id
|
||||
c.Email = user.Email
|
||||
c.IssuedAt = jwt.NewNumericDate(time.Now())
|
||||
c.NotBefore = jwt.NewNumericDate(time.Now())
|
||||
c.Issuer = config.JwtIssuer()
|
||||
c.Audience = config.JwtAudience()
|
||||
c.ExpiresAt = jwt.NewNumericDate(time.Now().Add(VERIFY_EMAIL_EXPIRATION))
|
||||
c.Type = TypeVerifyEmail
|
||||
c.Scope = "verify-email"
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
|
||||
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err := token.SignedString(config.EmailTokenSecret())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mnrva-dev/owltier.com/server/db"
|
||||
"github.com/mnrva-dev/owltier.com/server/token"
|
||||
)
|
||||
|
||||
var ts string
|
||||
|
||||
// TODO Write actual unit tests
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ts = token.GenerateAccess(&db.UserSchema{
|
||||
Id: "id_1234",
|
||||
Username: "myusername",
|
||||
Email: "user@example.com",
|
||||
Scope: "admin",
|
||||
Policies: []string{"policy1", "policy2"},
|
||||
})
|
||||
m.Run()
|
||||
}
|
||||
func TestAccessIdentity(t *testing.T) {
|
||||
if ts == "" {
|
||||
t.Fatal("No token was created")
|
||||
}
|
||||
c, err := token.ValidateAccess(ts)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid token, got %v", err)
|
||||
}
|
||||
if c.Email != "user@example.com" || c.Id != "id_1234" || c.Username != "myusername" {
|
||||
t.Errorf("Unexpected identity, got %v", c)
|
||||
}
|
||||
}
|
||||
func TestAccessScope(t *testing.T) {
|
||||
c, err := token.ValidateAccess(ts)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid token, got %v", err)
|
||||
}
|
||||
if c.Scope != "admin" {
|
||||
t.Errorf("Unexpected scope, expected %v got %v", "admin", c.Scope)
|
||||
}
|
||||
otherts := token.GenerateAccess(&db.UserSchema{
|
||||
Id: "id_1234",
|
||||
Username: "myusername",
|
||||
Email: "user@example.com",
|
||||
Policies: []string{"policy1", "policy2"},
|
||||
})
|
||||
c, err = token.ValidateAccess(otherts)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid token, got %v", err)
|
||||
}
|
||||
if c.Scope != "default" {
|
||||
t.Errorf("Unexpected scope, expected %v got %v", "default", c.Scope)
|
||||
}
|
||||
}
|
||||
func TestAccessPolicies(t *testing.T) {
|
||||
ts := token.GenerateAccess(&db.UserSchema{
|
||||
Id: "id_1234",
|
||||
Username: "myusername",
|
||||
Email: "user@example.com",
|
||||
Scope: "admin",
|
||||
Policies: []string{"policy1", "policy2"},
|
||||
})
|
||||
if ts == "" {
|
||||
t.Error("No token was created")
|
||||
}
|
||||
c, err := token.ValidateAccess(ts)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid token, got %v", err)
|
||||
}
|
||||
if len(c.Policies) != 2 || c.Policies[0] != "policy1" || c.Policies[1] != "policy2" {
|
||||
t.Errorf("Unexpected policies, got %v", c.Policies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
ts := token.GenerateRefresh(&db.UserSchema{
|
||||
Id: "12345",
|
||||
})
|
||||
if ts == "" {
|
||||
t.Error("No token was created")
|
||||
}
|
||||
c, err := token.ValidateRefresh(ts)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid token, got %v", err)
|
||||
}
|
||||
if c.Id != "12345" {
|
||||
t.Errorf("Unexpected identity, got %v", c)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package token
|
||||
|
||||
const (
|
||||
TypeRefresh = "Refresh"
|
||||
TypeAccess = "Access"
|
||||
TypeVerifyEmail = "VerifyEmail"
|
||||
)
|
||||
@ -0,0 +1,66 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/mnrva-dev/owltier.com/server/config"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func ValidateAccess(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return config.AccessSecret(), nil
|
||||
})
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok {
|
||||
if !token.Valid {
|
||||
return nil, fmt.Errorf("token is not valid")
|
||||
}
|
||||
if !slices.Contains(claims.Audience, "https://gosuimg.com") {
|
||||
return nil, fmt.Errorf("unexpected audience value: %v", claims.Audience)
|
||||
}
|
||||
if claims.Type != "Access" {
|
||||
return nil, fmt.Errorf("Unexpected token type: %v", claims.Type)
|
||||
}
|
||||
return claims, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateRefresh(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return config.RefreshSecret(), nil
|
||||
})
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
if !slices.Contains(claims.Audience, "https://gosuimg.com") {
|
||||
return &Claims{}, fmt.Errorf("unexpected audience value: %v", claims.Audience)
|
||||
}
|
||||
if claims.Type != "Refresh" {
|
||||
return &Claims{}, fmt.Errorf("Unexpected token type: %v", claims.Type)
|
||||
}
|
||||
return claims, nil
|
||||
} else {
|
||||
return &Claims{}, err
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateVerifyEmail(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return config.RefreshSecret(), nil
|
||||
})
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
if !slices.Contains(claims.Audience, "https://gosuimg.com") {
|
||||
return &Claims{}, fmt.Errorf("unexpected audience value: %v", claims.Audience)
|
||||
}
|
||||
if claims.Type != "VerifyEmail" {
|
||||
return &Claims{}, fmt.Errorf("Unexpected token type: %v", claims.Type)
|
||||
}
|
||||
return claims, nil
|
||||
} else {
|
||||
return &Claims{}, err
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue