Updated refresh token endpoint to enable rotation
This commit is contained in:
@@ -8,13 +8,11 @@ DATABASE_PORT=5432
|
|||||||
|
|
||||||
ACCESS_TOKEN_PRIVATE_KEY=
|
ACCESS_TOKEN_PRIVATE_KEY=
|
||||||
ACCESS_TOKEN_PUBLIC_KEY=
|
ACCESS_TOKEN_PUBLIC_KEY=
|
||||||
ACCESS_TOKEN_EXPIRED_IN=15m
|
ACCESS_TOKEN_MAXAGE=5
|
||||||
ACCESS_TOKEN_MAXAGE=15
|
|
||||||
|
|
||||||
REFRESH_TOKEN_PRIVATE_KEY=
|
REFRESH_TOKEN_PRIVATE_KEY=
|
||||||
REFRESH_TOKEN_PUBLIC_KEY=
|
REFRESH_TOKEN_PUBLIC_KEY=
|
||||||
REFRESH_TOKEN_EXPIRED_IN=60m
|
REFRESH_TOKEN_MAXAGE=30
|
||||||
REFRESH_TOKEN_MAXAGE=60
|
|
||||||
|
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|||||||
@@ -150,11 +150,10 @@ impl FromRequest for JwtAuth {
|
|||||||
};
|
};
|
||||||
let user_email = match conn.get::<_, String>(access_token_uuid.clone().to_string()) {
|
let user_email = match conn.get::<_, String>(access_token_uuid.clone().to_string()) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(err) => {
|
Err(_) => {
|
||||||
error!("Failed to get access token from redis: {}", err);
|
|
||||||
return ready(Err(ActixError::from(ServiceError {
|
return ready(Err(ActixError::from(ServiceError {
|
||||||
status: 500,
|
status: 404,
|
||||||
message: format!("Failed to get access token from redis: {}", err)
|
message: format!("Access token was not found")
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -163,9 +162,9 @@ impl FromRequest for JwtAuth {
|
|||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
ready(Ok(JwtAuth { token: access_token_uuid, user: user.into() }))
|
ready(Ok(JwtAuth { token: access_token_uuid, user: user.into() }))
|
||||||
}
|
}
|
||||||
Err(err) => return ready(Err(ActixError::from(ServiceError {
|
Err(_) => return ready(Err(ActixError::from(ServiceError {
|
||||||
status: 500,
|
status: 404,
|
||||||
message: format!("Failed to get user from db: {}", err)
|
message: format!("User was not found")
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::env;
|
|||||||
use actix_web::{get, post, web, HttpResponse, ResponseError, cookie::{Cookie, time::Duration}, HttpRequest};
|
use actix_web::{get, post, web, HttpResponse, ResponseError, cookie::{Cookie, time::Duration}, HttpRequest};
|
||||||
use log::error;
|
use log::error;
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
use siren::ServiceError;
|
use siren::ServiceError;
|
||||||
|
|
||||||
use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, db};
|
use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, db};
|
||||||
@@ -124,8 +125,21 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct RefreshParams {
|
||||||
|
refresh_token_rotation: Option<bool>
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/refresh")]
|
#[get("/refresh")]
|
||||||
async fn refresh(req: HttpRequest) -> HttpResponse {
|
async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||||
|
let params = match web::Query::<RefreshParams>::from_query(req.query_string()) {
|
||||||
|
Ok(params) => params,
|
||||||
|
Err(err) => return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 422,
|
||||||
|
message: err.to_string()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let refresh_token = match req.cookie("refresh_token") {
|
let refresh_token = match req.cookie("refresh_token") {
|
||||||
Some(cookie) => cookie.value().to_string(),
|
Some(cookie) => cookie.value().to_string(),
|
||||||
None => return ResponseError::error_response(&ServiceError {
|
None => return ResponseError::error_response(&ServiceError {
|
||||||
@@ -133,6 +147,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
message: "Refresh token not found".to_string()
|
message: "Refresh token not found".to_string()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY")
|
let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY")
|
||||||
.expect("REFRESH_TOKEN_PUBLIC_KEY must be set");
|
.expect("REFRESH_TOKEN_PUBLIC_KEY must be set");
|
||||||
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
||||||
@@ -151,9 +166,9 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
let redis_result: redis::RedisResult<String> = conn.get(refresh_token_details.token_uuid.to_string()).await;
|
let redis_result: redis::RedisResult<String> = conn.get(refresh_token_details.token_uuid.to_string()).await;
|
||||||
let email = match redis_result {
|
let email = match redis_result {
|
||||||
Ok(email) => email,
|
Ok(email) => email,
|
||||||
Err(err) => return ResponseError::error_response(&ServiceError {
|
Err(_) => return ResponseError::error_response(&ServiceError {
|
||||||
status: 500,
|
status: 404,
|
||||||
message: format!("Failed to get refresh token from redis: {}", err)
|
message: format!("Refresh token was not found")
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,6 +182,23 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Delete old auth token if it exists
|
||||||
|
match req.cookie("access_token") {
|
||||||
|
Some(cookie) => {
|
||||||
|
let access_token = cookie.value().to_string();
|
||||||
|
let public_key = env::var("ACCESS_TOKEN_PUBLIC_KEY")
|
||||||
|
.expect("ACCESS_TOKEN_PUBLIC_KEY must be set");
|
||||||
|
match verify_token(&access_token, &public_key) {
|
||||||
|
Ok(token_details) => {
|
||||||
|
let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await;
|
||||||
|
|
||||||
|
},
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
None => {}
|
||||||
|
};
|
||||||
|
|
||||||
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
||||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||||
.parse::<i64>()
|
.parse::<i64>()
|
||||||
@@ -194,10 +226,54 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
|
|
||||||
let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap();
|
let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap();
|
||||||
|
|
||||||
|
// Refresh the refresh token if requested
|
||||||
|
let refresh_token_rotation = match params.refresh_token_rotation {
|
||||||
|
Some(refresh_token_rotation) => refresh_token_rotation,
|
||||||
|
None => false
|
||||||
|
};
|
||||||
|
if refresh_token_rotation {
|
||||||
|
// Delete the old refresh token
|
||||||
|
let _: redis::RedisResult<()> = conn.del(refresh_token_details.token_uuid.to_string()).await;
|
||||||
|
|
||||||
|
let refresh_token_details = match generate_refresh_token(&refresh_token_details.email) {
|
||||||
|
Ok(token_details) => token_details,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to generate refresh token: {}", err);
|
||||||
|
return ResponseError::error_response(&err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE")
|
||||||
|
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
||||||
|
.parse::<i64>()
|
||||||
|
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||||
|
|
||||||
|
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &refresh_token_details.email, (refresh_token_max_age * 60) as usize).await;
|
||||||
|
if let Err(err) = refresh_result {
|
||||||
|
error!("Failed to set refresh token in redis: {}", err);
|
||||||
|
return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 500,
|
||||||
|
message: format!("Failed to set refresh token in redis: {}", err)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let refresh_cookie = Cookie::build("refresh_token", refresh_token_details.token.clone().unwrap())
|
||||||
|
.path("/")
|
||||||
|
.max_age(Duration::new(refresh_token_max_age * 60, 0))
|
||||||
|
.http_only(true)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.cookie(refresh_cookie)
|
||||||
|
.cookie(access_cookie)
|
||||||
|
.cookie(logged_in_cookie)
|
||||||
|
.json(JwtAuth { token: access_token_uuid, user: query_user.into() })
|
||||||
|
} else {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.cookie(access_cookie)
|
.cookie(access_cookie)
|
||||||
.cookie(logged_in_cookie)
|
.cookie(logged_in_cookie)
|
||||||
.json(JwtAuth { token: access_token_uuid, user: query_user.into() })
|
.json(JwtAuth { token: access_token_uuid, user: query_user.into() })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getRequest, postRequest } from '.';
|
import { getRequest, postRequest } from '.';
|
||||||
import { ResponseUser } from './auth.types';
|
import { RegisterUser, ResponseUser } from './auth.types';
|
||||||
|
|
||||||
export async function login(email: string, password: string): Promise<ResponseUser | undefined> {
|
export async function login(email: string, password: string): Promise<ResponseUser | undefined> {
|
||||||
const response = await postRequest('auth/login', { email, password }, { withCredentials: true });
|
const response = await postRequest('auth/login', { email, password }, { withCredentials: true });
|
||||||
@@ -10,10 +10,28 @@ export async function login(email: string, password: string): Promise<ResponseUs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function register(user: RegisterUser): Promise<boolean> {
|
||||||
|
const response = await postRequest('auth/register', user, { withCredentials: true });
|
||||||
|
if (response?.status === 201) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
return await postRequest('auth/logout', {}, { withCredentials: true });
|
return await postRequest('auth/logout', {}, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function refresh(refresh_token_rotation?: boolean): Promise<ResponseUser | undefined> {
|
||||||
|
const response = await getRequest('auth/refresh', { withCredentials: true, params: { refresh_token_rotation } });
|
||||||
|
if (response?.status === 200) {
|
||||||
|
return response.data as ResponseUser;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function me(): Promise<ResponseUser | undefined> {
|
export async function me(): Promise<ResponseUser | undefined> {
|
||||||
const response = await getRequest('auth/me', { withCredentials: true });
|
const response = await getRequest('auth/me', { withCredentials: true });
|
||||||
if (response?.status === 200) {
|
if (response?.status === 200) {
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ export interface ResponseUser {
|
|||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RegisterUser {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
// Home page for siren
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <></>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Anchor,
|
Anchor,
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Container,
|
Container,
|
||||||
Group,
|
Group,
|
||||||
@@ -16,13 +17,15 @@ import {
|
|||||||
PasswordInput,
|
PasswordInput,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
UnstyledButton
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { login, logout, me } from '@/api/auth';
|
import { login, register, logout, me } from '@/api/auth';
|
||||||
import { User } from '@/api/auth.types';
|
import { User } from '@/api/auth.types';
|
||||||
|
import { useToggle } from '@mantine/hooks';
|
||||||
|
|
||||||
interface HeaderItem {
|
interface HeaderItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -68,7 +71,7 @@ const headerItems: HeaderItem[] = [
|
|||||||
|
|
||||||
export default function Topbar() {
|
export default function Topbar() {
|
||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
const [showLogin, setShowLogin] = useState(false);
|
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
|
||||||
const [headers, setHeaders] = useState<HeaderItem[]>([]);
|
const [headers, setHeaders] = useState<HeaderItem[]>([]);
|
||||||
const [user, setUser] = useState<User | undefined>(undefined);
|
const [user, setUser] = useState<User | undefined>(undefined);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -108,15 +111,41 @@ export default function Topbar() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='user'>
|
<div className='user-section'>
|
||||||
<Menu shadow='md' width={200} trigger='hover' openDelay={100} closeDelay={400}>
|
{user ? (
|
||||||
|
<Menu shadow='md' width={200} openDelay={100} closeDelay={400}>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Avatar style={{ cursor: 'pointer' }} />
|
<UnstyledButton className='user user-button'>
|
||||||
|
<Group>
|
||||||
|
<Avatar />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text size='sm' fw={500}>
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text c='dimmed' size='xs'>
|
||||||
|
{user.role}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{!user && <Menu.Item onClick={() => setShowLogin(true)}>Login</Menu.Item>}
|
<Card>
|
||||||
{user && (
|
<Card.Section h={140} style={{}} />
|
||||||
<Menu.Item
|
<Avatar size={80} radius={80} mx={'auto'} mt={-30} />
|
||||||
|
<Text ta='center' fz='lg' fw={500} mt='sm'>
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</Text>
|
||||||
|
<Text ta='center' fz='sm' c='dimmed'>
|
||||||
|
{user.role}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
radius='md'
|
||||||
|
mt='xl'
|
||||||
|
size='md'
|
||||||
|
variant='default'
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const response = await logout();
|
const response = await logout();
|
||||||
if (response?.status == 200) {
|
if (response?.status == 200) {
|
||||||
@@ -126,28 +155,36 @@ export default function Topbar() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</Menu.Item>
|
</Button>
|
||||||
)}
|
</Card>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
) : (
|
||||||
|
<Group className='user'>
|
||||||
|
<Button onClick={() => toggle('login')}>Login</Button>
|
||||||
|
<Button variant='outline' onClick={() => toggle('register')}>
|
||||||
|
Sign up
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<LoginModal showLogin={showLogin} setShowLogin={setShowLogin} setUser={setUser} />
|
<LoginModal type={modalType} toggle={toggle} setUser={setUser} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoginModal({
|
interface LoginModalProps {
|
||||||
showLogin,
|
type?: string;
|
||||||
setShowLogin,
|
toggle: any;
|
||||||
setUser
|
|
||||||
}: {
|
|
||||||
showLogin: boolean;
|
|
||||||
setShowLogin: (show: boolean) => void;
|
|
||||||
setUser: (user: User) => void;
|
setUser: (user: User) => void;
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
function LoginModal({ type, toggle, setUser }: LoginModalProps) {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
remember: false
|
remember: false
|
||||||
@@ -155,19 +192,87 @@ function LoginModal({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function onClose() {
|
function onClose() {
|
||||||
setShowLogin(false);
|
toggle(undefined);
|
||||||
if (!form.values.remember) {
|
if (!form.values.remember) {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal opened={showLogin} onClose={onClose} withCloseButton={false}>
|
<Modal opened={type !== undefined} onClose={onClose} withCloseButton={false}>
|
||||||
|
{type == 'reset' ? (
|
||||||
|
<Container>
|
||||||
|
<Title ta='center'>Reset password</Title>
|
||||||
|
<Text c='dimmed' size='sm' ta='center' mt={5}>
|
||||||
|
Enter your email and we will send you a link to reset your password
|
||||||
|
</Text>
|
||||||
|
<Paper withBorder shadow='md' p={30} mt={30} radius='md'>
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(async (values) => {
|
||||||
|
const response = await login(values.email, values.password);
|
||||||
|
if (response) {
|
||||||
|
setUser(response.user);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TextInput label='Email' placeholder='you@example.com' required {...form.getInputProps('email')} />
|
||||||
|
<Button type='submit' fullWidth mt='xl'>
|
||||||
|
Reset password
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
) : type == 'register' ? (
|
||||||
|
<Container>
|
||||||
|
<Title ta='center'>Create account</Title>
|
||||||
|
<Text c='dimmed' size='sm' ta='center' mt={5}>
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Anchor size='sm' component='button' onClick={() => toggle('login')}>
|
||||||
|
Sign in
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper withBorder shadow='md' p={30} mt={30} radius='md'>
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(async (values) => {
|
||||||
|
const registerResponse = await register({
|
||||||
|
first_name: values.firstName,
|
||||||
|
last_name: values.lastName,
|
||||||
|
email: values.email,
|
||||||
|
password: values.password
|
||||||
|
});
|
||||||
|
if (registerResponse) {
|
||||||
|
const loginResponse = await login(values.email, values.password);
|
||||||
|
if (loginResponse) {
|
||||||
|
setUser(loginResponse.user);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TextInput label='First name' placeholder='John' required {...form.getInputProps('firstName')} />
|
||||||
|
<TextInput label='Last name' placeholder='Smith' required mt='md' {...form.getInputProps('lastName')} />
|
||||||
|
<TextInput label='Email' placeholder='you@example.com' required {...form.getInputProps('email')} />
|
||||||
|
<PasswordInput
|
||||||
|
label='Password'
|
||||||
|
placeholder='Your password'
|
||||||
|
required
|
||||||
|
mt='md'
|
||||||
|
{...form.getInputProps('password')}
|
||||||
|
/>
|
||||||
|
<Button type='submit' fullWidth mt='xl'>
|
||||||
|
Sign up
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
) : (
|
||||||
<Container>
|
<Container>
|
||||||
<Title ta='center'>Welcome back!</Title>
|
<Title ta='center'>Welcome back!</Title>
|
||||||
<Text c='dimmed' size='sm' ta='center' mt={5}>
|
<Text c='dimmed' size='sm' ta='center' mt={5}>
|
||||||
Do not have an account yet?{' '}
|
Do not have an account yet?{' '}
|
||||||
<Anchor size='sm' component='button'>
|
<Anchor size='sm' component='button' onClick={() => toggle('register')}>
|
||||||
Create account
|
Create account
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -192,7 +297,7 @@ function LoginModal({
|
|||||||
/>
|
/>
|
||||||
<Group justify='space-between' mt='lg'>
|
<Group justify='space-between' mt='lg'>
|
||||||
<Checkbox label='Remember me' {...form.getInputProps('remember')} />
|
<Checkbox label='Remember me' {...form.getInputProps('remember')} />
|
||||||
<Anchor component='button' size='sm'>
|
<Anchor component='button' size='sm' onClick={() => toggle('reset')}>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -202,6 +307,7 @@ function LoginModal({
|
|||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Container>
|
</Container>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .user {
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .title {
|
.navbar .title {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
padding-right: 2em;
|
padding-right: 2em;
|
||||||
@@ -25,20 +20,14 @@
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .avatar {
|
|
||||||
padding-right: 2em;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-items {
|
.header-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-items .header-item {
|
.header-items .header-item {
|
||||||
padding-left: 2em;
|
padding-left: 2rem;
|
||||||
padding-right: 2em;
|
padding-right: 2rem;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
}
|
}
|
||||||
@@ -50,3 +39,23 @@
|
|||||||
.header-items .active {
|
.header-items .active {
|
||||||
border-bottom: 2px solid #5f5f5f;
|
border-bottom: 2px solid #5f5f5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-section {
|
||||||
|
margin-left: 2rem;
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-button:hover {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user