...
 
Commits (13)
    https://gitcode.net/dashboards/tabler/-/commit/1f0e6e074ab47ce42fe989f918622a66dd3f89ed add google provider 2023-08-15T16:26:15+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/b7c772ce1b078e741d6c3e5f88606ef0c80a9ef1 styles fix 2023-08-15T21:18:11+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/8cf50584562b8d06793835affb70f7cb189b1f05 add google-brand icon 2023-08-15T21:50:59+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/e307ba44fbf66fe75b8021b8f136c1908f084a83 add credentials provider 2023-08-20T16:43:59+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/5250158600fdb6ad6d6c7d02360ee5067ecea4ac remove nextauth url 2023-08-20T16:51:43+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/8ffe0e6a1a54f9ccb4ab64e3c63aef70eeedbc11 auth fixes 2023-08-20T20:49:48+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/ebda434060e2810fe2d8e32ae80076086c5db562 add prisma generate 2023-08-20T22:03:49+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/361e81e4785dfed298b3d99632bc1ece50394757 postinstall prisma generate 2023-08-20T22:25:44+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/0efbb01e55cf3a8924fd54651ba89ce2dca135b5 fix package.json 2023-08-20T22:33:23+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/5edc93384c35dfa3345228632fe44a0650b4a490 fix error message 2023-08-21T22:30:49+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/020255f161b484c9d9ebdf4be9641a106a3207d5 auth error handling 2023-08-27T00:44:28+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/d673851db5e8720210a5eeb4ddb0b81194f50df0 replace credentails auth with auth0 2023-08-28T00:16:54+02:00 tabler.developer@gmail.com tabler.developer@gmail.com https://gitcode.net/dashboards/tabler/-/commit/7e62c3a5639be24b73272fe3afaf13acb97e63c8 add navigation auth 2023-08-31T02:11:31+02:00 tabler.developer@gmail.com tabler.developer@gmail.com
......@@ -32,6 +32,11 @@ POSTGRES_DATABASE=""
# Providers
GITHUB_ID=""
GITHUB_SECRET=""
GOOGLE_ID=""
GOOGLE_SECRET=""
AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
AUTH0_ISSUER=
# Auth config
NEXTAUTH_SECRET=""
......
import Signin from '@/components/layout/Signin';
import Signin from '@/components/Signin';
export const metadata = {
title: 'Tabler Sign in',
......
import Signup from '@/components/layout/Signup';
import Signup from '@/components/Signup';
export const metadata = {
title: 'Tabler Sign up',
......
// import prisma from '@/lib/prisma';
// import { hash } from 'bcrypt';
// import { NextResponse } from 'next/server';
// export async function POST(req: Request) {
// try {
// const { name, email, password } = (await req.json()) as {
// name: string;
// email: string;
// password: string;
// };
// if (!name || !email || !password) {
// throw { message: 'all fields are required' };
// }
// const hashedPassword = await hash(password, 12);
// const user = await prisma.user.create({
// data: {
// name,
// email: email.toLowerCase(),
// password: hashedPassword,
// },
// });
// return NextResponse.json({
// user: {
// name: user.name,
// email: user.email,
// },
// });
// } catch (error: any) {
// return new NextResponse(
// JSON.stringify({
// status: 'error',
// message: error.message,
// }),
// { status: 500 }
// );
// }
// }
\ No newline at end of file
......@@ -33,6 +33,23 @@ const icons = {
>
<path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
</svg>
),
'brand-google': ({ className }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={clsx('icon icon-tabler icon-tabler-brand-google', className)}
width={24}
height={24}
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M17.788 5.108a9 9 0 1 0 3.212 6.892h-8"></path>
</svg>
),
'brand-sketch': ({ className }) => (
<svg
......@@ -861,6 +878,44 @@ const icons = {
<path d="M14 4h6v4h-6z" />
</svg>
),
'logout': ({ className }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={clsx('icon icon-tabler icon-tabler-logout', className)}
width={24}
height={24}
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></path>
<path d="M9 12h12l-3 -3"></path>
<path d="M18 15l3 -3"></path>
</svg>
),
'rocket': ({ className }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={clsx('icon icon-tabler icon-tabler-rocket', className)}
width={24}
height={24}
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path>
<path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path>
<path d="M15 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
</svg>
),
'sun-moon': ({ className }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
......
'use client';
import Link from '@/components/Link';
import { useSearchParams } from 'next/navigation';
import Icon from '@/components/Icon';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { getNextAuthErrorMessage } from '@/helpers/index';
export default function Signin() {
// const router = useRouter();
const searchParams = useSearchParams();
// const [loading, setLoading] = useState(false);
// const [formValues, setFormValues] = useState({
// email: '',
// password: '',
// });
const nextAuthError = searchParams.get('error');
const [error] = useState(nextAuthError ? getNextAuthErrorMessage(nextAuthError) : '');
const callbackUrl = searchParams.get('callbackUrl') || '/';
// const onSubmit = async (e: React.FormEvent) => {
// e.preventDefault();
// try {
// setLoading(true);
// setFormValues({ email: '', password: '' });
// const res = await signIn('credentials', {
// redirect: false,
// email: formValues.email,
// password: formValues.password,
// callbackUrl,
// });
// setLoading(false);
// if (!res?.error) {
// router.push(callbackUrl);
// } else {
// setError('Invalid email or password');
// }
// } catch (error: any) {
// setLoading(false);
// setError(error);
// }
// };
// const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
// const { name, value } = event.target;
// setFormValues({ ...formValues, [name]: value });
// };
return (
<>
<div className="text-center mb-4">
<Link href="/" className="mx-auto d-inline-block logo" aria-label="Tabler" />
</div>
<div className="flex-column card card-md">
{/* <div className="card-body">
<h2 className="h2 text-center mb-4">Login to your account</h2>
{
error &&
<p className="text-center" style={{color: 'red'}}>{error}</p>
}
<form onSubmit={onSubmit}>
<div className="mb-3">
<label className="form-label">Email address</label>
<input
required
type="email"
name="email"
value={formValues.email}
onChange={handleChange}
className="form-control"
placeholder="your@email.com"
/>
</div>
<div className="mb-2">
<label className="form-label">
Password
</label>
<div className="input-group input-group-flat">
<input
required
type="password"
name="password"
value={formValues.password}
onChange={handleChange}
className="form-control"
placeholder="Your password"
/>
</div>
</div>
<div className="form-footer">
<button
type="submit"
className="btn btn-primary w-100"
disabled={loading}
>
{loading ? 'loading...' : 'Sign In'}
</button>
</div>
</form>
</div>*/}
<div className="card-body">
<h2 className="h2 text-center mb-4">Login to your account</h2>
{
error &&
<p className="text-center" style={{color: 'red'}}>{error}</p>
}
<div className="mb-2">
<label className="form-label text-center">
Using email and password
</label>
<div className="form-footer">
<button
className="btn btn-primary w-100"
onClick={() => signIn('auth0', { callbackUrl })}
>
Sign In
</button>
</div>
</div>
</div>
<div className="hr-text">or</div>
<div className="card-body">
<div className="row">
<div className="col">
<a onClick={() => signIn('github', { callbackUrl })} className="btn w-100">
<Icon name="brand-github"/>
Login with Github
</a>
</div>
<div className="col">
<a onClick={() => signIn('google', { callbackUrl })} className="btn w-100">
<Icon name="brand-google"/>
Login with Google
</a>
</div>
</div>
</div>
</div>
{/* <div className="text-center text-secondary mt-3">
Don't have account yet?
<a className="ml-2" onClick={() => router.push('/signup')}>
Sign up
</a>
</div> */}
</>
);
}
// 'use client';
// import Link from '@/components/Link';
// import { useRouter } from 'next/navigation';
// import { ChangeEvent, useState } from 'react';
// import { signIn } from 'next-auth/react';
// export default function Signup() {
// const router = useRouter();
// const [loading, setLoading] = useState(false);
// const [formValues, setFormValues] = useState({
// name: '',
// email: '',
// password: '',
// });
// const [error, setError] = useState('');
// const onSubmit = async (e: React.FormEvent) => {
// e.preventDefault();
// setLoading(true);
// setFormValues({ name: '', email: '', password: '' });
// try {
// const res = await fetch('/api/register', {
// method: 'POST',
// body: JSON.stringify(formValues),
// headers: {
// 'Content-Type': 'application/json',
// },
// });
// setLoading(false);
// if (!res.ok) {
// setError((await res.json()).message);
// return;
// }
// signIn(undefined, { callbackUrl: '/' });
// } catch (error: any) {
// setLoading(false);
// setError(error);
// }
// };
// const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
// const { name, value } = event.target;
// setFormValues({ ...formValues, [name]: value });
// };
// return (
// <>
// <div className="text-center mb-4">
// <Link href="/" className="mx-auto d-inline-block logo" aria-label="Tabler" />
// </div>
// <form onSubmit={onSubmit} className="flex-column card card-md">
// <div className="card-body">
// <h2 className="card-title text-center mb-4">Create new account</h2>
// {
// error &&
// <p className="text-center" style={{color: 'red'}}>{error}</p>
// }
// <div className="mb-3">
// <label className="form-label">Name</label>
// <input
// required
// type="name"
// name="name"
// value={formValues.name}
// onChange={handleChange}
// className="form-control"
// placeholder="Name"
// />
// </div>
// <div className="mb-3">
// <label className="form-label">Email address</label>
// <input
// required
// type="email"
// name="email"
// value={formValues.email}
// onChange={handleChange}
// className="form-control"
// placeholder="Enter email"
// />
// </div>
// <div className="mb-3">
// <label className="form-label">Password</label>
// <div className="input-group input-group-flat">
// <input
// type="password"
// name="password"
// value={formValues.password}
// onChange={handleChange}
// className="form-control"
// placeholder="Password"
// />
// </div>
// </div>
// <div className="form-footer">
// <button className="btn btn-primary w-100" disabled={loading}>
// {loading ? 'loading...' : 'Create new account'}
// </button>
// </div>
// </div>
// </form>
// <div className="text-center text-secondary mt-3">
// Already have account?
// <a className="ml-2" onClick={() => router.push('/api/auth/signin')}>
// Sign in</a>
// </div>
// </>
// );
// }
......@@ -200,6 +200,81 @@ const SidebarLink = (link, menu, onClick) => {
);
};
const NavigationAuth = () => {
const { data: session, status } = useSession();
const image = session?.user?.image;
const name = session?.user?.name;
const email = session?.user?.email;
const signIn = () => {
if (status === 'loading') return;
window.location.href = '/api/auth/signin';
};
return <div className="navbar-item d-flex items-center">
{
!session &&
<a onClick={() => signIn()} className={clsx('btn', { disabled: status === 'loading'})}>
Log in
</a>
}
{
session &&
<Popover className="navbar-dropdown">
{({ open }) => (
<>
<Popover.Button className={clsx('navbar-link d-flex items-center lh-1 text-reset p-0')}>
{
image
? <span
className="avatar avatar"
style={{
backgroundImage: `url(${image})`,
}}
/>
: <span className="avatar avatar text-center">
{name ? name.toUpperCase().substring(0, 1) : 'T'}
</span>
}
<div className="pl-2">
<small className="d-block">{name}</small>
{
email &&
<small className="mt-1 small text-muted">{session.user?.email}</small>
}
</div>
</Popover.Button>
<Popover.Panel className="navbar-dropdown-menu">
<div className="navbar-dropdown-menu-content">
<div className="navbar-dropdown-menu-link">
<div className="row items-center g-3">
<div className="col-auto">
<Shape icon='rocket'/>
</div>
<div className="col">
<h5>Subscription</h5>
</div>
</div>
</div>
<div onClick={() => signOut()} className="navbar-dropdown-menu-link">
<div className="row items-center g-3">
<div className="col-auto">
<Shape icon='logout'/>
</div>
<div className="col">
<h5>Log out</h5>
</div>
</div>
</div>
</div>
</Popover.Panel>
</>
)}
</Popover>
}
</div>;
};
const Navbar = ({
menu,
opened,
......@@ -211,28 +286,9 @@ const Navbar = ({
onClick?: (event: React.MouseEvent) => void;
className?: string
}) => {
const { data: session, status } = useSession();
return <div className={clsx('navbar', opened && 'opened', props.className)}>
{menuLinks.map((link) => (<Fragment key={link.menu}>{NavbarLink(link, menu)}</Fragment>))}
<div className="navbar-item">
{
!session &&
<a href="/api/auth/signin" className="btn">
{
status === 'loading'
? 'Validating session ...'
: 'Log in'
}
</a>
}
{
session &&
<a onClick={() => signOut()} className="btn">
Log out
</a>
}
</div>
<NavigationAuth/>
</div>;
};
......
'use client';
import Link from '@/components/Link';
import { useRouter } from 'next/navigation';
import Icon from '@/components/Icon';
import { signIn } from 'next-auth/react';
export default function Signin() {
const router = useRouter();'/';
const handleLogin = async (provider: 'github'): Promise<void> => {
await signIn(provider, {callbackUrl: '/'});
};
return (
<>
<div className="text-center mb-4">
<Link href="/" className="mx-auto d-inline-block logo" aria-label="Tabler" />
</div>
<div className="card card-md">
<div className="card-body">
<h2 className="h2 text-center mb-4">Login to your account</h2>
<form>
<div className="mb-3">
<label className="form-label">Email address</label>
<input type="email" className="form-control" placeholder="your@email.com"/>
</div>
<div className="mb-2">
<label className="form-label">
Password
</label>
<div className="input-group input-group-flat">
<input type="password" className="form-control" placeholder="Your password"/>
</div>
</div>
<div className="form-footer">
<button className="btn btn-primary w-100">Sign in</button>
</div>
</form>
</div>
<div className="hr-text">or</div>
<div className="card-body">
<div className="row">
<div className="col">
<a onClick={() => handleLogin('github')} className="btn w-100">
<Icon name="brand-github"/>
Login with Github
</a>
</div>
</div>
</div>
</div>
<div className="text-center text-secondary mt-3">
Don't have account yet?
<a className="ml-2" onClick={() => router.push('/signup')}>
Sign up
</a>
</div>
</>
);
}
'use client';
import Link from '@/components/Link';
import { useRouter } from 'next/navigation';
export default function Signup() {
const router = useRouter();
return (
<>
<div className="text-center mb-4">
<Link href="/" className="mx-auto d-inline-block logo" aria-label="Tabler" />
</div>
<form className="card card-md">
<div className="card-body">
<h2 className="card-title text-center mb-4">Create new account</h2>
<div className="mb-3">
<label className="form-label">Email address</label>
<input type="email" className="form-control" placeholder="Enter email"/>
</div>
<div className="mb-3">
<label className="form-label">Password</label>
<div className="input-group input-group-flat">
<input type="password" className="form-control" placeholder="Password"/>
</div>
</div>
<div className="form-footer">
<button className="btn btn-primary w-100">Create new account</button>
</div>
</div>
</form>
<div className="text-center text-secondary mt-3">
Already have account?
<a className="ml-2" onClick={() => router.push('/api/auth/signin')}>
Sign in</a>
</div>
</>
);
}
export const groupBy = function (xs, key) {
return xs.reduce(function (rv, x) {
;(rv[x[key]] = rv[x[key]] || []).push(x)
return rv
}, {})
}
;(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
export const sortByKeys = function (xs) {
return Object.keys(xs)
.sort()
.reduce((obj, key) => {
obj[key] = xs[key]
return obj
}, {})
}
obj[key] = xs[key];
return obj;
}, {});
};
export const toPascalCase = function (text: string) {
return text
.replace(new RegExp(/[-_]+/, "g"), " ")
.replace(new RegExp(/[^\w\s]/, "g"), "")
.replace(new RegExp(/[-_]+/, 'g'), ' ')
.replace(new RegExp(/[^\w\s]/, 'g'), '')
.replace(
new RegExp(/\s+(.)(\w+)/, "g"),
new RegExp(/\s+(.)(\w+)/, 'g'),
($1, $2, $3) => `${$2.toUpperCase() + $3.toLowerCase()}`
)
.replace(new RegExp(/\s/, "g"), "")
.replace(new RegExp(/\w/), (s) => s.toUpperCase())
}
.replace(new RegExp(/\s/, 'g'), '')
.replace(new RegExp(/\w/), (s) => s.toUpperCase());
};
export const baseUrl = {
development: "http://localhost:3000",
production: "https://tabler-icons.io",
}[process.env.NODE_ENV]
development: 'http://localhost:3000',
production: 'https://tabler-icons.io',
}[process.env.NODE_ENV];
export const getCurrentBrand = (hostname: string) => {
if (hostname && hostname.match(/tabler-icons/)) {
return "tabler-icons"
return 'tabler-icons';
}
return "tabler-ui"
}
return 'tabler-ui';
};
export const getNextAuthErrorMessage = (error: string): string => {
// Nextauth errors
// https://next-auth.js.org/configuration/pages#sign-in-page
// OAuthSignin: Error in constructing an authorization URL (1, 2, 3),
// OAuthCallback: Error in handling the response (1, 2, 3) from an OAuth provider.
// OAuthCreateAccount: Could not create OAuth provider user in the database.
// EmailCreateAccount: Could not create email provider user in the database.
// Callback: Error in the OAuth callback handler route
// OAuthAccountNotLinked: If the email on the account is already linked, but not with this OAuth account
// EmailSignin: Sending the e-mail with the verification token failed
// CredentialsSignin: The authorize callback returned null in the Credentials provider. We don't recommend providing information about which part of the credentials were wrong, as it might be abused by malicious hackers.
// SessionRequired: The content of this page requires you to be signed in at all times. See useSession for configuration.
// Default: Catch all, will apply, if none of the above matched
switch (error) {
case 'OAuthSignin':
case 'OAuthCallback':
case 'OAuthCreateAccount':
case 'EmailCreateAccount':
case 'Callback':
return 'Try signing in with a different account.';
case 'OAuthAccountNotLinked':
return 'To confirm your identity, sign in with the same account you used originally.';
case 'EmailSignin':
return 'The e-mail could not be sent.';
case 'CredentialsSignin':
return 'Sign in failed. Check the details you provided are correct.';
case 'SessionRequired':
return 'Please sign in to access this page.';
case 'Default':
default:
return 'Unable to sign in.';
}
};
\ No newline at end of file
import prisma from '@/lib/prisma';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import GitHubProvider from 'next-auth/providers/github';
import prisma from '@/lib/prisma';
import GoogleProvider from 'next-auth/providers/google';
import { NextAuthOptions } from 'next-auth';
import Auth0Provider from 'next-auth/providers/auth0';
export const authConfig = {
export const authConfig: NextAuthOptions = {
providers: [
GitHubProvider({
// CredentialsProvider({
// name: 'Login to your account',
// credentials: {
// email: {
// label: 'Email address',
// type: 'text',
// placeholder: 'your@email.com"'
// },
// password: {
// label: 'Password',
// type: 'password',
// placeholder: 'Your password'
// },
// },
// async authorize(credentials) {
// if (!credentials || !credentials.email || !credentials.password) {
// return null;
// }
// const user = await prisma.user.findUnique({
// where: { email: credentials.email }
// });
// if (!user) {
// return null;
// }
// const isPasswordValid = await compare(
// credentials.password,
// user.password
// );
// if (!isPasswordValid) {
// return null;
// }
// const { password, ...userWithoutPassword } = user;
// return userWithoutPassword as User;
// }
// }),
GitHubProvider({
clientId: process.env.GITHUB_ID as string,
clientSecret: process.env.GITHUB_SECRET as string,
}),
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID as string,
clientSecret: process.env.GOOGLE_SECRET as string,
}),
Auth0Provider({
clientId: process.env.AUTH0_CLIENT_ID as string,
clientSecret: process.env.AUTH0_CLIENT_SECRET as string,
issuer: process.env.AUTH0_ISSUER as string
}),
],
pages: {
signIn: '/signin',
signIn: '/signin'
},
adapter: PrismaAdapter(prisma),
secret: process.env.NEXTAUTH_SECRET,
database: process.env.POSTGRES_PRISMA_URL,
// session: {
// strategy: 'jwt',
// },
// callbacks: {
// session: ({ session, token }) => {
// return {
// ...session,
// user: {
// ...session.user,
// id: token.id,
// },
// };
// },
// jwt: ({ token, user }) => {
// if (user) {
// const u = user as unknown as any;
// return {
// ...token,
// id: u.id,
// };
// }
// return token;
// },
// },
};
......@@ -2,6 +2,7 @@
"private": true,
"sourceType": "module",
"scripts": {
"postinstall": "pnpm prisma generate",
"dev": "concurrently \"contentlayer dev\" \"next dev --port 3010\"",
"build": "contentlayer build && next build",
"start": "next start",
......
......@@ -3,28 +3,27 @@ generator client {
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
shadowDatabaseUrl = env("POSTGRES_URL_NON_POOLING") // used for migrations
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
shadowDatabaseUrl = env("POSTGRES_URL_NON_POOLING")
}
model Account {
id String @id @default(cuid())
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
refresh_token_expires_in Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
id String @id @default(cuid())
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
refresh_token_expires_in Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
......@@ -59,4 +58,4 @@ model VerificationToken {
@@unique([identifier, token])
@@map("verificationtokens")
}
\ No newline at end of file
}
......@@ -46,6 +46,7 @@
.icon {
width: divide(20, 16) * 1em;
min-width: divide(20, 16) * 1em;
height: divide(20, 16) * 1em;
vertical-align: bottom;
margin-right: $gap-2;
......
......@@ -21,6 +21,10 @@ Cards
.card-body {
padding: $gap-4;
flex: 1;
.card-md > & {
padding: 2.5rem;
}
}
.card-title {
......
......@@ -167,6 +167,7 @@ $grid-breakpoints: (
$container-max-width: px2rem(1280px);
$container-narrow-max-width: px2rem(990px);
$container-slim-max-width: px2rem(660px);
$container-tight-max-width: px2rem(500px);
$zindex-modal: 100;
$zindex-gototop: 90;
......@@ -177,6 +178,7 @@ $grid-columns: 12;
$header-height: 5rem;
$aside-width: 20rem;
$hr-margin-y: 2rem;
$form-focus-color: $color-primary;
$form-check-size: 1rem;
......
......@@ -72,6 +72,11 @@ $form-range-thumb-transition: background-color .3s ease-in-out, bor
}
}
.form-label {
display: block;
font-weight: $font-weight-medium;
margin-bottom: 0.5rem
}
.form-check {
@extend %form-common;
......@@ -135,7 +140,9 @@ $form-range-thumb-transition: background-color .3s ease-in-out, bor
}
}
.form-footer {
margin-top: 2rem;
}
.form-range {
......
.row > * {
min-width: 0;
}
.container,
.container-fluid {
width: 100%;
......@@ -10,6 +14,10 @@
}
}
.container-tight {
max-width: $container-tight-max-width;
}
.container-narrow {
max-width: $container-narrow-max-width;
}
......
......@@ -8,6 +8,40 @@ hr {
border-top: 1px solid $color-border-light;
}
.hr-text {
display: flex;
align-items: center;
margin: $hr-margin-y 0;
height: 1px;
&:after,
&:before {
flex: 1 1 auto;
height: 1px;
background-color: $color-border;
}
&:before {
content: "";
margin-right: .5rem;
}
&:after {
content: "";
margin-left: .5rem;
}
> *:first-child {
padding-right: .5rem;
padding-left: 0;
color: $color-border;
}
.card > & {
margin: 0;
}
}
a {
color: $color-primary;
text-decoration: none;
......