Merge branch 'security_branch' of https://github.com/lumijiez/ExpenseTrackerFAF into security_branch

# Conflicts:
#	src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java
#	src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java
#	src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java
This commit is contained in:
Dmitrii Cravcenco
2023-10-19 11:13:03 +03:00
39 changed files with 3774 additions and 24 deletions

View File

@@ -43,6 +43,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

View File

@@ -1,5 +1,8 @@
package com.faf223.expensetrackerfaf.config; package com.faf223.expensetrackerfaf.config;
import com.faf223.expensetrackerfaf.controller.auth.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -15,7 +18,6 @@ import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException; import java.io.IOException;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class JwtAuthenticationFilter extends OncePerRequestFilter {
@@ -35,23 +37,38 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
} }
final String authHeader = request.getHeader("Authorization"); final String authHeader = request.getHeader("Authorization");
final String jwt; final String jwt;
final String userEmail; String userEmail;
if (authHeader == null || !authHeader.startsWith("Bearer ")) { if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
jwt = authHeader.substring(7); jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { try {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); userEmail = jwtService.extractUsername(jwt);
if (jwtService.isTokenValid(jwt, userDetails)) { if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
userDetails, null, userDetails.getAuthorities()); if (jwtService.isTokenValid(jwt, userDetails)) {
authToken.setDetails(new WebAuthenticationDetailsSource() UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
.buildDetails(request)); userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} }
} catch (ExpiredJwtException e) {
// Token is expired; return a custom response
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
ErrorResponse errorResponse = new ErrorResponse("TokenExpired", "Your session has expired. Please log in again.");
ObjectMapper objectMapper = new ObjectMapper(); // You may need to import ObjectMapper
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
response.getWriter().flush();
return;
} }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
} }

View File

@@ -38,6 +38,10 @@ public class JwtService {
return generateToken(new HashMap<>(), userDetails); return generateToken(new HashMap<>(), userDetails);
} }
public String generateRefreshToken(UserDetails userDetails) {
return generateRefreshToken(new HashMap<>(), userDetails);
}
public String generateToken( public String generateToken(
Map<String, Object> extraClaims, Map<String, Object> extraClaims,
UserDetails userDetails UserDetails userDetails
@@ -45,6 +49,13 @@ public class JwtService {
return buildToken(extraClaims, userDetails, jwtExpiration); return buildToken(extraClaims, userDetails, jwtExpiration);
} }
public String generateRefreshToken(
Map<String, Object> extraClaims,
UserDetails userDetails
) {
return buildToken(extraClaims, userDetails, refreshExpiration);
}
private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) { private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) {
return Jwts return Jwts
.builder() .builder()

View File

@@ -1,13 +1,25 @@
package com.faf223.expensetrackerfaf.config; package com.faf223.expensetrackerfaf.config;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -23,7 +35,7 @@ public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.csrf(csrf -> csrf.disable()) .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll() .requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
@@ -31,7 +43,34 @@ public class SecurityConfiguration {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider) .authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // will be executed before UsernamePasswordAuthenticationFilter .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // will be executed before UsernamePasswordAuthenticationFilter
// .oauth2Login(Customizer.withDefaults());
return http.build(); return http.build();
} }
@Bean
public ClientRegistrationRepository clientRegistrationRepository(
@Value("${spring.security.oauth2.client.registration.google.client-id}") String clientId,
@Value("${spring.security.oauth2.client.registration.google.client-secret}") String clientSecret) {
ClientRegistration registration = ClientRegistration.withRegistrationId("google")
.clientId(clientId)
.clientSecret(clientSecret)
.clientName("Google")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://localhost:8081/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email")
.authorizationUri("https://accounts.google.com/o/oauth2/auth")
.tokenUri("https://accounts.google.com/o/oauth2/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.build();
return new InMemoryClientRegistrationRepository(registration);
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService() {
return new DefaultOAuth2UserService();
}
} }

View File

@@ -1,19 +1,17 @@
package com.faf223.expensetrackerfaf.controller.auth; package com.faf223.expensetrackerfaf.controller.auth;
import com.faf223.expensetrackerfaf.service.AuthenticationService; import com.faf223.expensetrackerfaf.service.AuthenticationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping("api/v1/auth") @RequestMapping("api/v1/auth")
@RequiredArgsConstructor
public class AuthenticationController { public class AuthenticationController {
private final AuthenticationService service; private final AuthenticationService service;
public AuthenticationController(AuthenticationService service) {
this.service = service;
}
@PostMapping("/register") @PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(@RequestBody RegisterRequest request) { public ResponseEntity<AuthenticationResponse> register(@RequestBody RegisterRequest request) {
return ResponseEntity.ok(service.register(request)); return ResponseEntity.ok(service.register(request));
@@ -24,8 +22,8 @@ public class AuthenticationController {
return ResponseEntity.ok(service.authenticate(request)); return ResponseEntity.ok(service.authenticate(request));
} }
@PostMapping("/refresh") @PostMapping("/refreshtoken")
public ResponseEntity<AuthenticationResponse> refreshAccessToken(@RequestBody TokenRefreshRequest refreshRequest) { public ResponseEntity<AuthenticationResponse> refreshAccessToken(@RequestBody TokenRefreshRequest request) {
return ResponseEntity.ok(service.refreshAccessToken(refreshRequest)); return ResponseEntity.ok(service.refreshAccessToken(request));
} }
} }

View File

@@ -0,0 +1,16 @@
package com.faf223.expensetrackerfaf.controller.auth;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ErrorResponse {
private String error;
private String message;
public ErrorResponse(String error, String message) {
this.error = error;
this.message = message;
}
}

View File

@@ -44,7 +44,7 @@ public class AuthenticationService {
UserDetails userDetails = new PersonDetails(credential); UserDetails userDetails = new PersonDetails(credential);
String jwtToken = jwtService.generateToken(userDetails); String jwtToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateToken(userDetails); String refreshToken = jwtService.generateRefreshToken(userDetails);
return AuthenticationResponse.builder() return AuthenticationResponse.builder()
.accessToken(jwtToken) .accessToken(jwtToken)
@@ -59,7 +59,7 @@ public class AuthenticationService {
UserDetails userDetails = new PersonDetails(credential); UserDetails userDetails = new PersonDetails(credential);
String jwtToken = jwtService.generateToken(userDetails); String jwtToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateToken(userDetails); String refreshToken = jwtService.generateRefreshToken(userDetails);
return AuthenticationResponse.builder() return AuthenticationResponse.builder()
.accessToken(jwtToken) .accessToken(jwtToken)
.refreshToken(refreshToken) .refreshToken(refreshToken)
@@ -76,9 +76,9 @@ public class AuthenticationService {
String jwtToken = jwtService.generateToken(userDetails); String jwtToken = jwtService.generateToken(userDetails);
return AuthenticationResponse.builder() return AuthenticationResponse.builder()
.accessToken(jwtToken) .accessToken(jwtToken)
.refreshToken(refreshToken) // Return the same refresh token .refreshToken(refreshToken)
.build(); .build();
}else { } else {
throw new RuntimeException("Invalid or expired refresh token"); throw new RuntimeException("Invalid or expired refresh token");
} }
} }

View File

@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -0,0 +1,14 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'plugin:svelte/recommended', 'prettier'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
}
};

View File

@@ -0,0 +1,12 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
.vercel
.output
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -0,0 +1,9 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -0,0 +1,6 @@
# ExpenseTracker App
![image](https://github.com/lumijiez/svelte-expensetracker/assets/59575049/7ceab8aa-6d85-4a6b-95fd-2cffbe505fdf)
Responsive flexbox dashboard made with Chart.js and Svelte

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "expensetracker",
"version": "0.0.1",
"scripts": {
"g": "vite dev -- --open",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@fontsource/fira-mono": "^4.5.10",
"@neoconfetti/svelte": "^1.0.0",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"chart.js": "^4.4.0",
"email-validator": "^2.0.4",
"svelte-fa": "^3.0.4"
}
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
}
</style>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,20 @@
<script>
import LoginForm from './LoginForm.svelte';
</script>
<div id="wrapper">
<LoginForm />
</div>
<style>
#wrapper {
background-color: #041721;
margin:0;
padding:0;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,168 @@
<script>
import * as EmailValidator from 'email-validator';
let isErrorVisible = false;
let username, password;
let message = ""
function submitForm(event) {
event.preventDefault();
console.log("Tried to submit!");
console.log("Valid? ", (validateEmail() && validatePassword() ? "Yes" : "No"));
}
function validateEmail() {
let valid = EmailValidator.validate(username);
isErrorVisible = valid ? false : true;
message = isErrorVisible ? "Invalid e-mail!" : "";
return valid;
}
function validatePassword() {
let valid = password.value != '';
isErrorVisible = valid ? false : true;
message = isErrorVisible ? "Invalid password!" : "";
return valid;
}
</script>
<div class="animated bounceInDown">
<div class="container">
{#if isErrorVisible}
<span class="error animated tada" id="msg">{message}</span>
{/if}
<form name="loginForm" class="loginForm" on:submit={submitForm}>
<h1 id="formTitle">Track<span>.io</span></h1>
<h5>Sign in to your account.</h5>
<input id="usernameInput" type="text" name="email" placeholder="Email or Username" autocomplete="off" on:input={
event => {username = event.target.value}
}>
<input id="passwordInput" type="password" name="password" placeholder="Password" autocomplete="off" on:input={
event => {password = event.target.password}
}>
<a href="/auth/recovery" class="recoveryPass">Forgot your password?</a>
<input type="submit" value="Sign in" class="submitButton">
</form>
<a href="/auth/register" class="noAccount">Don't have an account? Sign up</a>
</div>
</div>
<style>
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400');
.container {
margin: 0;
top: 50px;
left: 50%;
font-family: 'Source Sans Pro', sans-serif;
position: absolute;
text-align: center;
transform: translateX(-50%);
background-color: rgb(33, 41, 66);
border-radius: 9px;
border-top: 10px solid #79a6fe;
border-bottom: 10px solid #8BD17C;
width: 400px;
height: 500px;
box-shadow: 1px 1px 108.8px 19.2px rgb(25, 31, 53);
}
#formTitle {
font-family: 'Source Sans Pro', sans-serif;
color: #5c6bc0;
margin-top: 94px;
}
#formTitle span {
color: #dfdeee;
font-weight: lighter;
}
.loginForm h5 {
font-family: 'Source Sans Pro', sans-serif;
font-size: 13px;
color: #a1a4ad;
letter-spacing: 1.5px;
margin-top: -15px;
margin-bottom: 70px;
}
.loginForm input[type="text"],
.loginForm input[type="password"] {
display: block;
margin: 20px auto;
background: #262e49;
border: 0;
border-radius: 5px;
padding: 14px 10px;
width: 320px;
outline: none;
color: #d6d6d6;
-webkit-transition: all .2s ease-out;
-moz-transition: all .2s ease-out;
-ms-transition: all .2s ease-out;
-o-transition: all .2s ease-out;
transition: all .2s ease-out;
}
.loginForm input[type="text"]:focus,
.loginForm input[type="password"]:focus {
border: 1px solid #79A6FE;
}
a {
color: #5c7fda;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.submitButton {
border: 0;
background: #7f5feb;
color: #dfdeee;
border-radius: 100px;
width: 340px;
height: 49px;
font-size: 16px;
position: absolute;
top: 79%;
left: 8%;
transition: 0.3s;
cursor: pointer;
}
.submitButton:hover {
background: #5d33e6;
}
.recoveryPass {
position: relative;
float: right;
right: 28px;
}
.noAccount {
position: absolute;
top: 92%;
left: 24%;
}
.error {
text-align: center;
width: 337px;
height: 20px;
padding: 2px;
border: 0;
border-radius: 5px;
margin: 10px auto 10px;
position: absolute;
top: 31%;
left: 7.2%;
color: rgb(190, 67, 29);
}
</style>

View File

@@ -0,0 +1,20 @@
<script>
import RegisterForm from './RegisterForm.svelte';
</script>
<div id="wrapper">
<RegisterForm />
</div>
<style>
#wrapper {
background-color: #041721;
margin:0;
padding:0;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,178 @@
<script>
import * as EmailValidator from 'email-validator';
let isErrorVisible = false;
let username, email, password;
let message = ""
function submitForm(event) {
event.preventDefault();
console.log("Tried to submit!");
console.log("Valid? ", (validateEmail() && validateUsername() && va && validatePassword() ? "Yes" : "No"));
}
function validateEmail() {
let valid = EmailValidator.validate(username);
isErrorVisible = valid ? false : true;
message = isErrorVisible ? "Invalid e-mail!" : "";
return valid;
}
function validatePassword() {
let valid = password.value != '';
isErrorVisible = valid ? false : true;
message = isErrorVisible ? "Invalid password!" : "";
return valid;
}
function validateUsername() {
let valid = username.value != '';
isErrorVisible = valid ? false : true;
message = isErrorVisible ? "Invalid password!" : "";
return valid;
}
</script>
<div class="animated bounceInDown">
<div class="container">
{#if isErrorVisible}
<span class="error animated tada" id="msg">{message}</span>
{/if}
<form name="registerForm" class="registerForm" on:submit={submitForm}>
<h1 id="formTitle">Track<span>.io</span></h1>
<h5>Sign up for a new account.</h5>
<input id="usernameInput" type="text" name="username" placeholder="Username" autocomplete="off" on:input={
event => {username = event.target.value}
}>
<input id="emailInput" type="text" name="email" placeholder="Email" autocomplete="off" on:input={
event => {email = event.target.value}
}>
<input id="passwordInput" type="password" name="password" placeholder="Password" autocomplete="off" on:input={
event => {password = event.target.password}
}>
<a href="/auth/recovery" class="recoveryPass">Forgot your password?</a>
<input type="submit" value="Sign up" class="submitButton">
</form>
<a href="/auth/login" class="noAccount">Already have an account? Sign in</a>
</div>
</div>
<style>
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400');
.container {
margin: 0;
top: 50px;
left: 50%;
font-family: 'Source Sans Pro', sans-serif;
position: absolute;
text-align: center;
transform: translateX(-50%);
background-color: rgb(33, 41, 66);
border-radius: 9px;
border-top: 10px solid #79a6fe;
border-bottom: 10px solid #8BD17C;
width: 400px;
height: 600px;
box-shadow: 1px 1px 108.8px 19.2px rgb(25, 31, 53);
}
#formTitle {
font-family: 'Source Sans Pro', sans-serif;
color: #5c6bc0;
margin-top: 94px;
}
#formTitle span {
color: #dfdeee;
font-weight: lighter;
}
.registerForm h5 {
font-family: 'Source Sans Pro', sans-serif;
font-size: 13px;
color: #a1a4ad;
letter-spacing: 1.5px;
margin-top: -15px;
margin-bottom: 70px;
}
.registerForm input[type="text"],
.registerForm input[type="password"] {
display: block;
margin: 20px auto;
background: #262e49;
border: 0;
border-radius: 5px;
padding: 14px 10px;
width: 320px;
outline: none;
color: #d6d6d6;
-webkit-transition: all .2s ease-out;
-moz-transition: all .2s ease-out;
-ms-transition: all .2s ease-out;
-o-transition: all .2s ease-out;
transition: all .2s ease-out;
}
.registerForm input[type="text"]:focus,
.registerForm input[type="password"]:focus {
border: 1px solid #79A6FE;
}
a {
color: #5c7fda;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.submitButton {
border: 0;
background: #7f5feb;
color: #dfdeee;
border-radius: 100px;
width: 340px;
height: 49px;
font-size: 16px;
position: absolute;
top: 79%;
left: 8%;
transition: 0.3s;
cursor: pointer;
}
.submitButton:hover {
background: #5d33e6;
}
.recoveryPass {
position: relative;
float: right;
right: 28px;
}
.noAccount {
position: absolute;
top: 92%;
left: 24%;
}
.error {
text-align: center;
width: 337px;
height: 20px;
padding: 2px;
border: 0;
border-radius: 5px;
margin: 10px auto 10px;
position: absolute;
top: 31%;
left: 7.2%;
color: rgb(190, 67, 29);
}
</style>

View File

@@ -0,0 +1,20 @@
<script>
import Dashboard from './board/Dashboard.svelte';
import SideMenu from './menu/SideMenu.svelte';
</script>
<div id="wrapper">
<SideMenu />
<Dashboard />
</div>
<style>
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400');
#wrapper {
background-color: rgb(23,34,51);
display: flex;
align-items: stretch;
min-height: 100vh;
}
</style>

View File

@@ -0,0 +1,66 @@
<script>
</script>
<div id="header">
<div id="dashboardTitleWrapper">
<h5>Hello, welcome to your</h5>
<h1 id="dashboardTitle">Dashboard</h1>
</div>
<div id="icons">
<div class="headerbtn searchButton">
<svg xmlns="http://www.w3.org/2000/svg" height="1.3em" viewBox="0 0 512 512"><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>
</div>
<div class="headerbtn notificationButton">
<svg xmlns="http://www.w3.org/2000/svg" height="1.3em" viewBox="0 0 448 512"><path d="M224 0c-17.7 0-32 14.3-32 32V49.9C119.5 61.4 64 124.2 64 200v33.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V200c0-75.8-55.5-138.6-128-150.1V32c0-17.7-14.3-32-32-32zm0 96h8c57.4 0 104 46.6 104 104v33.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V200c0-57.4 46.6-104 104-104h8zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"/></svg>
</div>
</div>
</div>
<style>
#header {
display: flex;
align-items: center;
justify-content: space-between;
}
#dashboardTitleWrapper {
display: flex;
flex-direction: column;
margin:20px;
margin-bottom: 0px;
}
#dashboardTitleWrapper h5 {
margin: 0;
}
#dashboardTitleWrapper h1 {
margin-top: 0;
}
#icons {
display: flex;
margin-right:20px;
}
.headerbtn {
width: 70px;
height: 70px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.headerbtn:hover::before {
content: "";
width: 40px;
height: 40px;
background-color: rgb(100, 100, 100, 0.25);
border-radius: 50%;
position: absolute;
}
</style>

View File

@@ -0,0 +1,27 @@
<script>
import DashHeader from "./DashHeader.svelte";
import DataMenu from "./DataMenu.svelte";
import QuickInfobar from "./QuickInfobar.svelte";
import NotificationBoard from "./NotificationBoard.svelte";
</script>
<div id="dashboard">
<DashHeader />
<QuickInfobar />
<DataMenu />
</div>
<style>
#dashboard {
font-family: 'Source Sans Pro', sans-serif;
background-color: rgb(245,242,243);
border-radius: 20px;
margin: 20px;
min-width: 100px;
display: flex;
flex:1 1 auto;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
}
</style>

View File

@@ -0,0 +1,65 @@
<script>
import Graph1 from './graphs/Graph1.svelte';
import Graph2 from './graphs/Graph2.svelte';
import Graph3 from './graphs/Graph3.svelte';
import Graph4 from './graphs/Graph4.svelte';
import Graph5 from './graphs/Graph5.svelte';
</script>
<div id="dataMenu">
<div id="twoVertical">
<Graph1 />
<Graph2 />
</div>
<div id="oneVertical">
<Graph3 />
</div>
<div id="twoHorizontal">
<Graph4 />
<Graph5 />
</div>
</div>
<style>
#dataMenu {
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
background-color: rgb(245,242,243);
display:flex;
padding:10px;
flex-direction: row-reverse;
justify-content: stretch;
align-items: stretch;
flex-grow: 1;
}
#twoVertical {
display: flex;
flex-direction: column;
align-self: stretch;
flex-grow: 1;
min-width: 0;
min-height:0;
}
#oneVertical {
display: flex;
flex-direction: column;
align-self: stretch;
flex-grow: 1;
min-width: 0;
min-height:0;
}
#twoHorizontal {
display: flex;
flex-direction: column;
align-self: stretch;
flex-grow: 1;
min-width: 0;
min-height:0;
}
</style>

View File

@@ -0,0 +1,15 @@
<script>
</script>
<div id="notificationBoard">
</div>
<style>
#notificationBoard {
display: none;
width:0;
height:0;
}
</style>

View File

@@ -0,0 +1,37 @@
<script>
</script>
<div id="quickInfobar">
<div class="infobarElement">Income for this month 50$</div>
<div class="infobarElement">Expense for this month 40$</div>
<div class="infobarElement">Income for this month 50$</div>
<div class="infobarElement">Expense for this month 40$</div>
</div>
<style>
#quickInfobar {
display: flex;
justify-content: space-between;
margin: 20px;
}
.infobarElement {
margin: 10px;
width: 200px;
min-width: 100px;
height: 100px;
padding: 10px;
border-radius: 10px;
background-color: #ffdde2;
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
/* border: 1px solid black; */
}
.infobarElement:hover {
/* color:white; */
/* background-color: black; */
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
}
</style>

View File

@@ -0,0 +1,48 @@
<script>
import Chart from 'chart.js/auto';
import { onMount } from 'svelte';
let chartValues = [20, 10, 5, 2, 20, 30, 45];
let chartLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
let ctx;
let chartCanvas;
onMount(async () => {
ctx = chartCanvas.getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: chartLabels,
datasets: [{
label: 'Revenue',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: chartValues
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
});
</script>
<div id="chart">
<canvas bind:this={chartCanvas}></canvas>
</div>
<style>
#chart {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
flex: 1;
border-radius: 10px;
margin:10px;
background-color: #ffdde2;
}
#chart:hover {
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
}
</style>

View File

@@ -0,0 +1,66 @@
<script>
import chartjs from 'chart.js/auto';
import { onMount } from 'svelte';
var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var config = {
type: 'line',
data: {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [{
fill: false,
data: [
5,
6,
3,
4
]
}, {
label: "My Second dataset ",
fill: false,
data: [
5,
6,
3,
4
]
}]
},
options: {
title:{
display:true,
text: "Chart.js Line Chart - Animation Progress Bar"
},
maintainAspectRatio: false
}
};
let chartValues = [20, 10, 5, 2, 20, 30, 45];
let chartLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
let ctx;
let chartCanvas;
onMount(async (promise) => {
ctx = chartCanvas.getContext('2d');
var chart = new chartjs(ctx, config);
});
</script>
<div id="chart">
<canvas bind:this={chartCanvas}></canvas>
</div>
<style>
#chart {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
flex: 1;
border-radius: 10px;
margin:10px;
background-color: #ffdde2;
}
#chart:hover {
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
}
</style>

View File

@@ -0,0 +1,63 @@
<script>
import chartjs from 'chart.js/auto';
import { onMount } from 'svelte';
var randomScalingFactor = function() {
return Math.round(Math.random() * 100);
};
var config = {
type: 'pie',
data: {
datasets: [{
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
],
label: 'Dataset 1'
}],
labels: [
"Red",
"Orange",
"Yellow",
"Green",
"Blue"
]
},
options: {
responsive: true,
maintainAspectRatio: false
}
};
let ctx;
let chartCanvas;
onMount(async (promise) => {
ctx = chartCanvas.getContext('2d');
var chart = new chartjs(ctx, config);
});
</script>
<div id="chart">
<canvas bind:this={chartCanvas}></canvas>
</div>
<style>
#chart {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
flex: 1;
border-radius: 10px;
margin:10px;
background-color: #ffdde2;
}
#chart:hover {
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
}
</style>

View File

@@ -0,0 +1,48 @@
<script>
import chartjs from 'chart.js/auto';
import { onMount } from 'svelte';
let chartValues = [20, 10, 5, 2, 20, 30, 45];
let chartLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
let ctx;
let chartCanvas;
onMount(async (promise) => {
ctx = chartCanvas.getContext('2d');
var chart = new chartjs(ctx, {
type: 'line',
data: {
labels: chartLabels,
datasets: [{
label: 'Revenue',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: chartValues
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
});
</script>
<div id="chart">
<canvas bind:this={chartCanvas}></canvas>
</div>
<style>
#chart {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
flex: 1;
border-radius: 10px;
margin:10px;
background-color: #ffdde2;
}
#chart:hover {
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
}
</style>

View File

@@ -0,0 +1,48 @@
<script>
import chartjs from 'chart.js/auto';
import { onMount } from 'svelte';
let chartValues = [20, 10, 5, 2, 20, 30, 45];
let chartLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
let ctx;
let chartCanvas;
onMount(async (promise) => {
ctx = chartCanvas.getContext('2d');
var chart = new chartjs(ctx, {
type: 'line',
data: {
labels: chartLabels,
datasets: [{
label: 'Revenue',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: chartValues
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
});
</script>
<div id="chart">
<canvas bind:this={chartCanvas}></canvas>
</div>
<style>
#chart {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
flex: 1;
border-radius: 10px;
margin:10px;
background-color: #ffdde2;
}
#chart:hover {
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
}
</style>

View File

@@ -0,0 +1,47 @@
<script>
</script>
<div id="navbar">
<div id="profilePic">PROFILEPIC</div>
<div class="menuItem" on:click={() => console.log("help clicked")}>Help</div>
<div class="menuItem" on:click={() => console.log("contact clicked")}>Contact</div>
<div class="menuItem" on:click={() => console.log("settings clicked")}>Settings</div>
<div class="menuItem" on:click={() => console.log("logout clicked")}>Logout</div>
</div>
<style>
#navbar {
padding:0;
background-color: rgb(33, 41, 66);
display: flex;
justify-content: flex-end;
}
.menuItem {
font-family: 'Source Sans Pro', sans-serif;
color:white;
display: flex;
align-items: center;
padding-left: 20px;
padding-right: 20px;
text-align: center;
height: 50px;
}
.menuItem:hover {
background-color: rgb(45, 60, 90);
}
#profilePic {
margin-right:auto;
display: flex;
padding-left: 20px;
padding-right: 20px;
align-items: center;
color:white;
justify-self: left;
}
</style>

View File

@@ -0,0 +1,95 @@
<script>
</script>
<div id="sideMenu">
<div id="iconSpace">
<div id="icon">
<img id="iconImg" src='./../../../src/lib/images/adidas.png' alt="icon"/>
</div>
</div>
<div id="menuSpace">
<div class="sideMenuItem">
<svg class="svgimg" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>
<span class="sideMenuItemText">Profile</span>
</div>
<div class="sideMenuItem">
<svg class="svgimg" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 576 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zm64 320H64V320c35.3 0 64 28.7 64 64zM64 192V128h64c0 35.3-28.7 64-64 64zM448 384c0-35.3 28.7-64 64-64v64H448zm64-192c-35.3 0-64-28.7-64-64h64v64zM288 160a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/></svg>
<span class="sideMenuItemText">Expenses</span>
</div>
<div class="sideMenuItem">
<svg class="svgimg" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 576 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zm64 320H64V320c35.3 0 64 28.7 64 64zM64 192V128h64c0 35.3-28.7 64-64 64zM448 384c0-35.3 28.7-64 64-64v64H448zm64-192c-35.3 0-64-28.7-64-64h64v64zM288 160a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/></svg>
<span class="sideMenuItemText">Incomes</span>
</div>
<div class="sideMenuItem">
<svg class="svgimg" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 576 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zm64 320H64V320c35.3 0 64 28.7 64 64zM64 192V128h64c0 35.3-28.7 64-64 64zM448 384c0-35.3 28.7-64 64-64v64H448zm64-192c-35.3 0-64-28.7-64-64h64v64zM288 160a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/></svg>
<span class="sideMenuItemText">General</span>
</div>
<div class="sideMenuItem">
<svg class="svgimg" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg>
<span class="sideMenuItemText">Settings</span>
</div>
</div>
<div id="profileSpace">
<div id="profileInfo">Profile Info</div>
</div>
</div>
<style>
#sideMenu {
font-family: 'Source Sans Pro', sans-serif;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 150px;
max-width: 200px;
margin-left: 20px;
}
#iconSpace {
margin-top:20px;
display: flex;
align-items: center;
justify-content: center;
}
.sideMenuItem {
min-height: 50px;
color:white;
display: flex;
justify-content: center;
align-items: center;
border-radius: 20px;
}
.sideMenuItem:hover {
background-color: rgb(45, 60, 90);
}
.sideMenuItemText {
padding:10px;
}
.svgimg {
fill:white;
}
#iconImg {
max-width: 100px;
}
#profileSpace {
margin-bottom: 20px;
display: flex;
justify-content: center;
color:white;
font-weight: 900;
font-size: larger;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,13 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

View File

@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});