Compare commits
31 Commits
solid-urql
...
auth-backe
| Author | SHA1 | Date | |
|---|---|---|---|
| e107d358ee | |||
| fc24ce1306 | |||
| d527500034 | |||
| 637dfb45d8 | |||
| 65d62e8e49 | |||
| 6d19022c64 | |||
| fb0ca6a3ba | |||
| 2a70074382 | |||
| 31e7aec64d | |||
| 1bcf265aec | |||
| 6f5cc1642a | |||
| cd28031637 | |||
| 8bed3632a2 | |||
| e06850eec4 | |||
| 48545030fd | |||
| 3ce2a12861 | |||
| 9e76c140e4 | |||
| 37585ff9c4 | |||
| c44be1bcc1 | |||
| 6bb920861a | |||
| 40eb1da379 | |||
| 8a9973565b | |||
| ec84da2f52 | |||
| bb92669508 | |||
| b4171e215d | |||
| 5b9cbd62fd | |||
| 083b7177d3 | |||
| 87aa032b6b | |||
| 75e0ad2f6f | |||
| ec92cc4787 | |||
| f4162f7f06 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -57,7 +57,7 @@ node_modules
|
|||||||
/build
|
/build
|
||||||
/.svelte-kit
|
/.svelte-kit
|
||||||
/package
|
/package
|
||||||
.env
|
#.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
.vercel
|
.vercel
|
||||||
@@ -65,3 +65,5 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
.graphqlrc.yml
|
.graphqlrc.yml
|
||||||
codegen.yml
|
codegen.yml
|
||||||
|
backend/src/config/kc.config.json
|
||||||
|
backend/.env
|
||||||
|
|||||||
25
backend/.eslintrc.js
Normal file
25
backend/.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir : __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
35
backend/.gitignore
vendored
Normal file
35
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
4
backend/.prettierrc
Normal file
4
backend/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
73
backend/README.md
Normal file
73
backend/README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](LICENSE).
|
||||||
11
backend/graphql/generate.typings.ts
Normal file
11
backend/graphql/generate.typings.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const definitionFactory = new GraphQLDefinitionsFactory();
|
||||||
|
|
||||||
|
definitionFactory.generate({
|
||||||
|
typePaths: ['./**/*.graphql'],
|
||||||
|
path: join(process.cwd(), 'src/graphql/graphql.typings.ts'),
|
||||||
|
outputAs: 'class',
|
||||||
|
watch: true,
|
||||||
|
});
|
||||||
35
backend/graphql/graphql.typings.ts
Normal file
35
backend/graphql/graphql.typings.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------------------------------------
|
||||||
|
* THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||||
|
* -------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export class CreateUserInput {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
time_joined: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class IQuery {
|
||||||
|
abstract users(): Nullable<User>[] | Promise<Nullable<User>[]>;
|
||||||
|
|
||||||
|
abstract user(id: string): Nullable<User> | Promise<Nullable<User>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class IMutation {
|
||||||
|
abstract createUser(createUserInput: CreateUserInput): User | Promise<User>;
|
||||||
|
|
||||||
|
abstract removeUser(id: string): Nullable<User> | Promise<Nullable<User>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTime = any;
|
||||||
|
type Nullable<T> = T | null;
|
||||||
5
backend/nest-cli.json
Normal file
5
backend/nest-cli.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src"
|
||||||
|
}
|
||||||
79
backend/package.json
Normal file
79
backend/package.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "rimraf dist",
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"gen:types": "ts-node ./graphql/generate.typings",
|
||||||
|
"prisma:gen": "prisma generate --watch",
|
||||||
|
"dev": "concurrently \"npm:start:dev\" \"npm:gen:types\" \"npm:prisma:gen\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/apollo": "^10.1.0",
|
||||||
|
"@nestjs/common": "^9.0.0",
|
||||||
|
"@nestjs/config": "^2.2.0",
|
||||||
|
"@nestjs/core": "^9.0.0",
|
||||||
|
"@nestjs/graphql": "^10.1.1",
|
||||||
|
"@nestjs/mapped-types": "*",
|
||||||
|
"@nestjs/platform-express": "^9.0.0",
|
||||||
|
"@prisma/client": "4.3.1",
|
||||||
|
"apollo-server-express": "^3.10.2",
|
||||||
|
"graphql": "^16.6.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"rxjs": "^7.2.0",
|
||||||
|
"supertokens-node": "^11.3.0",
|
||||||
|
"ts-morph": "^16.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^9.0.0",
|
||||||
|
"@nestjs/schematics": "^9.0.0",
|
||||||
|
"@nestjs/testing": "^9.0.0",
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/jest": "28.1.8",
|
||||||
|
"@types/node": "^16.0.0",
|
||||||
|
"@types/supertest": "^2.0.11",
|
||||||
|
"concurrently": "^7.4.0",
|
||||||
|
"jest": "28.1.3",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"prisma": "4.3.1",
|
||||||
|
"source-map-support": "^0.5.20",
|
||||||
|
"supertest": "^6.1.3",
|
||||||
|
"ts-jest": "28.0.8",
|
||||||
|
"ts-loader": "^9.2.3",
|
||||||
|
"ts-node": "^10.0.0",
|
||||||
|
"tsconfig-paths": "4.1.0",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
backend/prisma/migrations/20220907234937_init/migration.sql
Normal file
11
backend/prisma/migrations/20220907234937_init/migration.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"time_joined" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "password" TEXT,
|
||||||
|
ALTER COLUMN "time_joined" DROP NOT NULL;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ADD COLUMN "updatedAt" TIMESTAMP(3);
|
||||||
245
backend/prisma/migrations/20220908224452_/migration.sql
Normal file
245
backend/prisma/migrations/20220908224452_/migration.sql
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "User";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "all_auth_recipe_users" (
|
||||||
|
"user_id" CHAR(36) NOT NULL,
|
||||||
|
"recipe_id" VARCHAR(128) NOT NULL,
|
||||||
|
"time_joined" BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "all_auth_recipe_users_pkey" PRIMARY KEY ("user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "emailpassword_pswd_reset_tokens" (
|
||||||
|
"user_id" CHAR(36) NOT NULL,
|
||||||
|
"token" VARCHAR(128) NOT NULL,
|
||||||
|
"token_expiry" BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "emailpassword_pswd_reset_tokens_pkey" PRIMARY KEY ("user_id","token")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "emailpassword_users" (
|
||||||
|
"user_id" CHAR(36) NOT NULL,
|
||||||
|
"email" VARCHAR(256) NOT NULL,
|
||||||
|
"password_hash" VARCHAR(128) NOT NULL,
|
||||||
|
"time_joined" BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "emailpassword_users_pkey" PRIMARY KEY ("user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "emailverification_tokens" (
|
||||||
|
"user_id" VARCHAR(128) NOT NULL,
|
||||||
|
"email" VARCHAR(256) NOT NULL,
|
||||||
|
"token" VARCHAR(128) NOT NULL,
|
||||||
|
"token_expiry" BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "emailverification_tokens_pkey" PRIMARY KEY ("user_id","email","token")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "emailverification_verified_emails" (
|
||||||
|
"user_id" VARCHAR(128) NOT NULL,
|
||||||
|
"email" VARCHAR(256) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "emailverification_verified_emails_pkey" PRIMARY KEY ("user_id","email")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "jwt_signing_keys" (
|
||||||
|
"key_id" VARCHAR(255) NOT NULL,
|
||||||
|
"key_string" TEXT NOT NULL,
|
||||||
|
"algorithm" VARCHAR(10) NOT NULL,
|
||||||
|
"created_at" BIGINT,
|
||||||
|
|
||||||
|
CONSTRAINT "jwt_signing_keys_pkey" PRIMARY KEY ("key_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "key_value" (
|
||||||
|
"name" VARCHAR(128) NOT NULL,
|
||||||
|
"value" TEXT,
|
||||||
|
"created_at_time" BIGINT,
|
||||||
|
|
||||||
|
CONSTRAINT "key_value_pkey" PRIMARY KEY ("name")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "passwordless_codes" (
|
||||||
|
"code_id" CHAR(36) NOT NULL,
|
||||||
|
"device_id_hash" CHAR(44) NOT NULL,
|
||||||
|
"link_code_hash" CHAR(44) NOT NULL,
|
||||||
|
"created_at" BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "passwordless_codes_pkey" PRIMARY KEY ("code_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "passwordless_devices" (
|
||||||
|
"device_id_hash" CHAR(44) NOT NULL,
|
||||||
|
"email" VARCHAR(256),
|
||||||
|
"phone_number" VARCHAR(256),
|
||||||
|
"link_code_salt" CHAR(44) NOT NULL,
|
||||||
|
"failed_attempts" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "passwordless_devices_pkey" PRIMARY KEY ("device_id_hash")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "passwordless_users" (
|
||||||
|
"user_id" CHAR(36) NOT NULL,
|
||||||
|
"email" VARCHAR(256),
|
||||||
|
"phone_number" VARCHAR(256),
|
||||||
|
"time_joined" BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "passwordless_users_pkey" PRIMARY KEY ("user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "role_permissions" (
|
||||||
|
"role" VARCHAR(255) NOT NULL,
|
||||||
|
"permission" VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("role","permission")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "roles" (
|
||||||
|
"role" VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "roles_pkey" PRIMARY KEY ("role")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "session_access_token_signing_keys" (
|
||||||
|
"created_at_time" BIGINT NOT NULL,
|
||||||
|
"value" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "session_access_token_signing_keys_pkey" PRIMARY KEY ("created_at_time")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "session_info" (
|
||||||
|
"session_handle" VARCHAR(255) NOT NULL,
|
||||||
|
"user_id" VARCHAR(128) NOT NULL,
|
||||||
|
"refresh_token_hash_2" VARCHAR(128) NOT NULL,
|
||||||
|
"session_data" TEXT,
|
||||||
|
"expires_at" BIGINT NOT NULL,
|
||||||
|
"created_at_time" BIGINT NOT NULL,
|
||||||
|
"jwt_user_payload" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "session_info_pkey" PRIMARY KEY ("session_handle")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "thirdparty_users" (
|
||||||
|
"third_party_id" VARCHAR(28) NOT NULL,
|
||||||
|
"third_party_user_id" VARCHAR(128) NOT NULL,
|
||||||
|
"user_id" CHAR(36) NOT NULL,
|
||||||
|
"email" VARCHAR(256) NOT NULL,
|
||||||
|
"time_joined" BIGINT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "thirdparty_users_pkey" PRIMARY KEY ("third_party_id","third_party_user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user_metadata" (
|
||||||
|
"user_id" VARCHAR(128) NOT NULL,
|
||||||
|
"user_metadata" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "user_metadata_pkey" PRIMARY KEY ("user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user_roles" (
|
||||||
|
"user_id" VARCHAR(128) NOT NULL,
|
||||||
|
"role" VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "user_roles_pkey" PRIMARY KEY ("user_id","role")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "userid_mapping" (
|
||||||
|
"supertokens_user_id" CHAR(36) NOT NULL,
|
||||||
|
"external_user_id" VARCHAR(128) NOT NULL,
|
||||||
|
"external_user_id_info" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "userid_mapping_pkey" PRIMARY KEY ("supertokens_user_id","external_user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "all_auth_recipe_users_pagination_index" ON "all_auth_recipe_users"("time_joined" DESC, "user_id" DESC);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "emailpassword_pswd_reset_tokens_token_key" ON "emailpassword_pswd_reset_tokens"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "emailpassword_password_reset_token_expiry_index" ON "emailpassword_pswd_reset_tokens"("token_expiry");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "emailpassword_users_email_key" ON "emailpassword_users"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "emailverification_tokens_token_key" ON "emailverification_tokens"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "emailverification_tokens_index" ON "emailverification_tokens"("token_expiry");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "passwordless_codes_link_code_hash_key" ON "passwordless_codes"("link_code_hash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "passwordless_codes_created_at_index" ON "passwordless_codes"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "passwordless_codes_device_id_hash_index" ON "passwordless_codes"("device_id_hash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "passwordless_devices_email_index" ON "passwordless_devices"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "passwordless_devices_phone_number_index" ON "passwordless_devices"("phone_number");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "passwordless_users_email_key" ON "passwordless_users"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "passwordless_users_phone_number_key" ON "passwordless_users"("phone_number");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "role_permissions_permission_index" ON "role_permissions"("permission");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "thirdparty_users_user_id_key" ON "thirdparty_users"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "user_roles_role_index" ON "user_roles"("role");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "userid_mapping_supertokens_user_id_key" ON "userid_mapping"("supertokens_user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "userid_mapping_external_user_id_key" ON "userid_mapping"("external_user_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "emailpassword_pswd_reset_tokens" ADD CONSTRAINT "emailpassword_pswd_reset_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "emailpassword_users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "passwordless_codes" ADD CONSTRAINT "passwordless_codes_device_id_hash_fkey" FOREIGN KEY ("device_id_hash") REFERENCES "passwordless_devices"("device_id_hash") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_fkey" FOREIGN KEY ("role") REFERENCES "roles"("role") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_fkey" FOREIGN KEY ("role") REFERENCES "roles"("role") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "userid_mapping" ADD CONSTRAINT "userid_mapping_supertokens_user_id_fkey" FOREIGN KEY ("supertokens_user_id") REFERENCES "all_auth_recipe_users"("user_id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
15
backend/prisma/prisma.service.ts
Normal file
15
backend/prisma/prisma.service.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableShutdownHooks(app: INestApplication) {
|
||||||
|
this.$on('beforeExit', async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
158
backend/prisma/schema.prisma
Normal file
158
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model all_auth_recipe_users {
|
||||||
|
user_id String @id @db.Char(36)
|
||||||
|
recipe_id String @db.VarChar(128)
|
||||||
|
time_joined BigInt
|
||||||
|
userid_mapping userid_mapping?
|
||||||
|
|
||||||
|
@@index([time_joined(sort: Desc), user_id(sort: Desc)], map: "all_auth_recipe_users_pagination_index")
|
||||||
|
}
|
||||||
|
|
||||||
|
model emailpassword_pswd_reset_tokens {
|
||||||
|
user_id String @db.Char(36)
|
||||||
|
token String @unique @db.VarChar(128)
|
||||||
|
token_expiry BigInt
|
||||||
|
emailpassword_users emailpassword_users @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([user_id, token])
|
||||||
|
@@index([token_expiry], map: "emailpassword_password_reset_token_expiry_index")
|
||||||
|
}
|
||||||
|
|
||||||
|
model emailpassword_users {
|
||||||
|
user_id String @id @db.Char(36)
|
||||||
|
email String @unique @db.VarChar(256)
|
||||||
|
password_hash String @db.VarChar(128)
|
||||||
|
time_joined BigInt
|
||||||
|
emailpassword_pswd_reset_tokens emailpassword_pswd_reset_tokens[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model emailverification_tokens {
|
||||||
|
user_id String @db.VarChar(128)
|
||||||
|
email String @db.VarChar(256)
|
||||||
|
token String @unique @db.VarChar(128)
|
||||||
|
token_expiry BigInt
|
||||||
|
|
||||||
|
@@id([user_id, email, token])
|
||||||
|
@@index([token_expiry], map: "emailverification_tokens_index")
|
||||||
|
}
|
||||||
|
|
||||||
|
model emailverification_verified_emails {
|
||||||
|
user_id String @db.VarChar(128)
|
||||||
|
email String @db.VarChar(256)
|
||||||
|
|
||||||
|
@@id([user_id, email])
|
||||||
|
}
|
||||||
|
|
||||||
|
model jwt_signing_keys {
|
||||||
|
key_id String @id @db.VarChar(255)
|
||||||
|
key_string String
|
||||||
|
algorithm String @db.VarChar(10)
|
||||||
|
created_at BigInt?
|
||||||
|
}
|
||||||
|
|
||||||
|
model key_value {
|
||||||
|
name String @id @db.VarChar(128)
|
||||||
|
value String?
|
||||||
|
created_at_time BigInt?
|
||||||
|
}
|
||||||
|
|
||||||
|
model passwordless_codes {
|
||||||
|
code_id String @id @db.Char(36)
|
||||||
|
device_id_hash String @db.Char(44)
|
||||||
|
link_code_hash String @unique @db.Char(44)
|
||||||
|
created_at BigInt
|
||||||
|
passwordless_devices passwordless_devices @relation(fields: [device_id_hash], references: [device_id_hash], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([created_at], map: "passwordless_codes_created_at_index")
|
||||||
|
@@index([device_id_hash], map: "passwordless_codes_device_id_hash_index")
|
||||||
|
}
|
||||||
|
|
||||||
|
model passwordless_devices {
|
||||||
|
device_id_hash String @id @db.Char(44)
|
||||||
|
email String? @db.VarChar(256)
|
||||||
|
phone_number String? @db.VarChar(256)
|
||||||
|
link_code_salt String @db.Char(44)
|
||||||
|
failed_attempts Int
|
||||||
|
passwordless_codes passwordless_codes[]
|
||||||
|
|
||||||
|
@@index([email], map: "passwordless_devices_email_index")
|
||||||
|
@@index([phone_number], map: "passwordless_devices_phone_number_index")
|
||||||
|
}
|
||||||
|
|
||||||
|
model passwordless_users {
|
||||||
|
user_id String @id @db.Char(36)
|
||||||
|
email String? @unique @db.VarChar(256)
|
||||||
|
phone_number String? @unique @db.VarChar(256)
|
||||||
|
time_joined BigInt
|
||||||
|
}
|
||||||
|
|
||||||
|
model role_permissions {
|
||||||
|
role String @db.VarChar(255)
|
||||||
|
permission String @db.VarChar(255)
|
||||||
|
roles roles @relation(fields: [role], references: [role], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
||||||
|
@@id([role, permission])
|
||||||
|
@@index([permission], map: "role_permissions_permission_index")
|
||||||
|
}
|
||||||
|
|
||||||
|
model roles {
|
||||||
|
role String @id @db.VarChar(255)
|
||||||
|
role_permissions role_permissions[]
|
||||||
|
user_roles user_roles[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model session_access_token_signing_keys {
|
||||||
|
created_at_time BigInt @id
|
||||||
|
value String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model session_info {
|
||||||
|
session_handle String @id @db.VarChar(255)
|
||||||
|
user_id String @db.VarChar(128)
|
||||||
|
refresh_token_hash_2 String @db.VarChar(128)
|
||||||
|
session_data String?
|
||||||
|
expires_at BigInt
|
||||||
|
created_at_time BigInt
|
||||||
|
jwt_user_payload String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model thirdparty_users {
|
||||||
|
third_party_id String @db.VarChar(28)
|
||||||
|
third_party_user_id String @db.VarChar(128)
|
||||||
|
user_id String @unique @db.Char(36)
|
||||||
|
email String @db.VarChar(256)
|
||||||
|
time_joined BigInt
|
||||||
|
|
||||||
|
@@id([third_party_id, third_party_user_id])
|
||||||
|
}
|
||||||
|
|
||||||
|
model user_metadata {
|
||||||
|
user_id String @id @db.VarChar(128)
|
||||||
|
user_metadata String
|
||||||
|
}
|
||||||
|
|
||||||
|
model user_roles {
|
||||||
|
user_id String @db.VarChar(128)
|
||||||
|
role String @db.VarChar(255)
|
||||||
|
roles roles @relation(fields: [role], references: [role], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
||||||
|
@@id([user_id, role])
|
||||||
|
@@index([role], map: "user_roles_role_index")
|
||||||
|
}
|
||||||
|
|
||||||
|
model userid_mapping {
|
||||||
|
supertokens_user_id String @unique @db.Char(36)
|
||||||
|
external_user_id String @unique @db.VarChar(128)
|
||||||
|
external_user_id_info String?
|
||||||
|
all_auth_recipe_users all_auth_recipe_users @relation(fields: [supertokens_user_id], references: [user_id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
||||||
|
@@id([supertokens_user_id, external_user_id])
|
||||||
|
}
|
||||||
19
backend/src/app.controller.ts
Normal file
19
backend/src/app.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Controller, Get, Session, UseGuards } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from './auth/guards/auth.guard';
|
||||||
|
import { SessionContainer } from "supertokens-node/recipe/session";
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return "API";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('test')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async getTest(@Session() session: SessionContainer): Promise<string> {
|
||||||
|
// TODO: magic
|
||||||
|
return "magic";
|
||||||
|
}
|
||||||
|
}
|
||||||
37
backend/src/app.module.ts
Normal file
37
backend/src/app.module.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { GraphQLISODateTime, GraphQLModule } from '@nestjs/graphql';
|
||||||
|
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||||
|
import { ApolloServerPluginLandingPageLocalDefault } from 'apollo-server-core';
|
||||||
|
// import { UsersModule } from './users/users.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot(),
|
||||||
|
AuthModule.forRoot({
|
||||||
|
connectionURI: process.env.ST_API_URL,
|
||||||
|
apiKey: process.env.ST_API_KEY,
|
||||||
|
appInfo: {
|
||||||
|
appName: process.env.APP_NAME,
|
||||||
|
apiDomain: process.env.APP_URL,
|
||||||
|
websiteDomain: process.env.WEBAPP_URL,
|
||||||
|
apiBasePath: '/auth/api',
|
||||||
|
websiteBasePath: '/auth',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||||
|
driver: ApolloDriver,
|
||||||
|
typePaths: ['./**/*.graphql'],
|
||||||
|
debug: true,
|
||||||
|
playground: false,
|
||||||
|
resolvers: { DateTime: GraphQLISODateTime },
|
||||||
|
plugins: [ApolloServerPluginLandingPageLocalDefault()],
|
||||||
|
}),
|
||||||
|
// UsersModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
43
backend/src/auth/auth.module.ts
Normal file
43
backend/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
DynamicModule,
|
||||||
|
MiddlewareConsumer,
|
||||||
|
Module,
|
||||||
|
NestModule,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthMiddleware } from './middlewares/auth.middleware';
|
||||||
|
import { AuthModuleConfig, ConfigInjectionToken } from './interfaces/config.interface';
|
||||||
|
import { SupertokensService } from './supertokens/supertokens.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [],
|
||||||
|
exports: [],
|
||||||
|
controllers: [],
|
||||||
|
})
|
||||||
|
export class AuthModule implements NestModule {
|
||||||
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
consumer.apply(AuthMiddleware).forRoutes('*');
|
||||||
|
}
|
||||||
|
|
||||||
|
static forRoot({
|
||||||
|
connectionURI,
|
||||||
|
apiKey,
|
||||||
|
appInfo,
|
||||||
|
}: AuthModuleConfig): DynamicModule {
|
||||||
|
return {
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
useValue: {
|
||||||
|
appInfo,
|
||||||
|
connectionURI,
|
||||||
|
apiKey,
|
||||||
|
},
|
||||||
|
provide: ConfigInjectionToken,
|
||||||
|
},
|
||||||
|
SupertokensService,
|
||||||
|
],
|
||||||
|
exports: [],
|
||||||
|
imports: [],
|
||||||
|
module: AuthModule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
8
backend/src/auth/decorators/session.decorator.ts
Normal file
8
backend/src/auth/decorators/session.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const Session = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.session;
|
||||||
|
},
|
||||||
|
);
|
||||||
29
backend/src/auth/filters/auth.filter.ts
Normal file
29
backend/src/auth/filters/auth.filter.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
|
||||||
|
import { errorHandler } from 'supertokens-node/framework/express';
|
||||||
|
import { Error as STError } from 'supertokens-node';
|
||||||
|
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
|
||||||
|
|
||||||
|
|
||||||
|
@Catch(STError)
|
||||||
|
export class SupertokensExceptionFilter implements ExceptionFilter {
|
||||||
|
handler: ErrorRequestHandler;
|
||||||
|
constructor() {
|
||||||
|
this.handler = errorHandler();
|
||||||
|
}
|
||||||
|
catch(exception: Error, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
|
||||||
|
const resp = ctx.getResponse<Response>();
|
||||||
|
if (resp.headersSent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handler(
|
||||||
|
exception,
|
||||||
|
ctx.getRequest<Request>(),
|
||||||
|
resp,
|
||||||
|
ctx.getNext<NextFunction>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
backend/src/auth/guards/auth.guard.ts
Normal file
31
backend/src/auth/guards/auth.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { Error as STError } from 'supertokens-node';
|
||||||
|
|
||||||
|
import { verifySession } from 'supertokens-node/recipe/session/framework/express';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const ctx = context.switchToHttp();
|
||||||
|
|
||||||
|
let err = undefined;
|
||||||
|
const resp = ctx.getResponse();
|
||||||
|
// You can create an optional version of this by passing {sessionRequired: false} to verifySession
|
||||||
|
await verifySession()(ctx.getRequest(), resp, (res) => {
|
||||||
|
err = res;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.headersSent) {
|
||||||
|
throw new STError({
|
||||||
|
message: 'RESPONSE_SENT',
|
||||||
|
type: 'RESPONSE_SENT',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/auth/interfaces/config.interface.ts
Normal file
9
backend/src/auth/interfaces/config.interface.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { AppInfo } from 'supertokens-node/types';
|
||||||
|
|
||||||
|
export const ConfigInjectionToken = 'ConfigInjectionToken';
|
||||||
|
|
||||||
|
export type AuthModuleConfig = {
|
||||||
|
appInfo: AppInfo;
|
||||||
|
connectionURI: string;
|
||||||
|
apiKey?: string;
|
||||||
|
};
|
||||||
15
backend/src/auth/middlewares/auth.middleware.ts
Normal file
15
backend/src/auth/middlewares/auth.middleware.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
|
import { middleware } from 'supertokens-node/framework/express';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthMiddleware implements NestMiddleware {
|
||||||
|
supertokensMiddleware: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.supertokensMiddleware = middleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
use(req: Request, res: any, next: () => void) {
|
||||||
|
return this.supertokensMiddleware(req, res, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
backend/src/auth/supertokens/supertokens.service.ts
Normal file
78
backend/src/auth/supertokens/supertokens.service.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
AuthModuleConfig,
|
||||||
|
ConfigInjectionToken,
|
||||||
|
} from '../interfaces/config.interface';
|
||||||
|
import supertokens from 'supertokens-node';
|
||||||
|
import ThirdPartyEmailPassword from 'supertokens-node/recipe/thirdpartyemailpassword';
|
||||||
|
import EmailPassword from 'supertokens-node/recipe/emailpassword';
|
||||||
|
import { STMPService } from 'supertokens-node/recipe/thirdpartyemailpassword/emaildelivery';
|
||||||
|
|
||||||
|
import Session from 'supertokens-node/recipe/session';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SupertokensService {
|
||||||
|
constructor(@Inject(ConfigInjectionToken) private config: AuthModuleConfig) {
|
||||||
|
supertokens.init({
|
||||||
|
appInfo: config.appInfo,
|
||||||
|
supertokens: {
|
||||||
|
connectionURI: config.connectionURI,
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
},
|
||||||
|
recipeList: [
|
||||||
|
EmailPassword.init({
|
||||||
|
emailDelivery: {
|
||||||
|
service: new STMPService({
|
||||||
|
smtpSettings: {
|
||||||
|
host: process.env.SMTP_HOST,
|
||||||
|
authUsername: process.env.SMTP_USERNAME,
|
||||||
|
password: process.env.SMTP_PASSWORD,
|
||||||
|
port: parseInt(process.env.SMTP_PORT),
|
||||||
|
from: {
|
||||||
|
name: process.env.SMTP_FROM_NAME,
|
||||||
|
email: process.env.SMTP_FROM_EMAIL,
|
||||||
|
},
|
||||||
|
secure: process.env.SMTP_SECURE ? true : false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ThirdPartyEmailPassword.init({
|
||||||
|
providers: [
|
||||||
|
ThirdPartyEmailPassword.Google({
|
||||||
|
clientId:
|
||||||
|
'1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com',
|
||||||
|
clientSecret: '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',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Session.init({
|
||||||
|
// antiCsrf: "VIA_CUSTOM_HEADER",
|
||||||
|
jwt: {
|
||||||
|
enable: true,
|
||||||
|
issuer: `${process.env.APP_URL}/auth/api`,
|
||||||
|
},
|
||||||
|
override: {
|
||||||
|
functions: function (originalImplementation) {
|
||||||
|
return {
|
||||||
|
...originalImplementation,
|
||||||
|
createNewSession: async function (input) {
|
||||||
|
input.accessTokenPayload = {
|
||||||
|
...input.accessTokenPayload,
|
||||||
|
'https://hasura.io/jwt/claims': {
|
||||||
|
'x-hasura-user-id': input.userId,
|
||||||
|
'x-hasura-default-role': 'user',
|
||||||
|
'x-hasura-allowed-roles': ['user'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return originalImplementation.createNewSession(input);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
48
backend/src/graphql/graphql.typings.ts
Normal file
48
backend/src/graphql/graphql.typings.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------------------------------------
|
||||||
|
* THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||||
|
* -------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export class CreateUserInput {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateUserInput {
|
||||||
|
email?: Nullable<string>;
|
||||||
|
password?: Nullable<string>;
|
||||||
|
time_joined?: Nullable<number>;
|
||||||
|
createdAt?: Nullable<DateTime>;
|
||||||
|
updatedAt?: Nullable<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
password?: Nullable<string>;
|
||||||
|
time_joined?: Nullable<number>;
|
||||||
|
createdAt?: Nullable<DateTime>;
|
||||||
|
updatedAt?: Nullable<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class IQuery {
|
||||||
|
abstract users(): Nullable<User>[] | Promise<Nullable<User>[]>;
|
||||||
|
|
||||||
|
abstract user(id: string): Nullable<User> | Promise<Nullable<User>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class IMutation {
|
||||||
|
abstract createUser(createUserInput: CreateUserInput): User | Promise<User>;
|
||||||
|
|
||||||
|
abstract updateUser(id: string, updateUserInput: UpdateUserInput): Nullable<User> | Promise<Nullable<User>>;
|
||||||
|
|
||||||
|
abstract removeUser(id: string): Nullable<User> | Promise<Nullable<User>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTime = any;
|
||||||
|
type Nullable<T> = T | null;
|
||||||
20
backend/src/main.ts
Normal file
20
backend/src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import supertokens from 'supertokens-node';
|
||||||
|
import { SupertokensExceptionFilter } from './auth/filters/auth.filter';
|
||||||
|
// import { PrismaService } from 'prisma/prisma.service';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.enableCors({
|
||||||
|
origin: [process.env.WEBAPP_URL, 'http://server.home:8081'],
|
||||||
|
allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()],
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
app.useGlobalFilters(new SupertokensExceptionFilter());
|
||||||
|
// const prismaService = app.get(PrismaService);
|
||||||
|
// await prismaService.enableShutdownHooks(app);
|
||||||
|
|
||||||
|
await app.listen(process.env.APP_PORT);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
34
backend/src/users/users.graphql
Normal file
34
backend/src/users/users.graphql
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
scalar DateTime
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
email: String!
|
||||||
|
password: String
|
||||||
|
time_joined: Int
|
||||||
|
createdAt: DateTime
|
||||||
|
updatedAt: DateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateUserInput {
|
||||||
|
email: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateUserInput {
|
||||||
|
email: String
|
||||||
|
password: String
|
||||||
|
time_joined: Int
|
||||||
|
createdAt: DateTime
|
||||||
|
updatedAt: DateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
users: [User]!
|
||||||
|
user(id: ID!): User
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createUser(createUserInput: CreateUserInput!): User!
|
||||||
|
updateUser(id: ID!, updateUserInput: UpdateUserInput!): User
|
||||||
|
removeUser(id: ID!): User
|
||||||
|
}
|
||||||
9
backend/src/users/users.module.ts
Normal file
9
backend/src/users/users.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { UsersResolver } from './users.resolver';
|
||||||
|
import { PrismaService } from 'prisma/prisma.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService, UsersResolver, UsersService],
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
||||||
32
backend/src/users/users.resolver.ts
Normal file
32
backend/src/users/users.resolver.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
|
@Resolver('User')
|
||||||
|
export class UsersResolver {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
// @Mutation('createUser')
|
||||||
|
// create(@Args('createUserInput') createUserInput: Prisma.UserCreateInput) {
|
||||||
|
// return this.usersService.create(createUserInput);
|
||||||
|
// }
|
||||||
|
// @Query('users')
|
||||||
|
// findAll(@Args('params') params?: Prisma.UserFindManyArgs) {
|
||||||
|
// return this.usersService.users(params);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Query('user')
|
||||||
|
// findOne(@Args('id') id: string) {
|
||||||
|
// return this.usersService.user({ id });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Mutation('updateUser')
|
||||||
|
// update(@Args('updateUserInput') updateUserInput: UpdateUserInput) {
|
||||||
|
// return this.usersService.update(updateUserInput.id, updateUserInput);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Mutation('removeUser')
|
||||||
|
// remove(@Args('id') id: string) {
|
||||||
|
// return this.usersService.remove(id);
|
||||||
|
// }
|
||||||
|
}
|
||||||
31
backend/src/users/users.service.ts
Normal file
31
backend/src/users/users.service.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
// import { Prisma, User } from '@prisma/client';
|
||||||
|
import { PrismaService } from 'prisma/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
// async user(uniqueInput: Prisma.UserWhereUniqueInput) {
|
||||||
|
// return await this.prismaService.user.findUnique({ where: uniqueInput });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async users(params?: Prisma.UserFindManyArgs) {
|
||||||
|
// return await this.prismaService.user.findMany(params);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// create(createUserInput: Prisma.UserCreateInput) {
|
||||||
|
// return this.prismaService.user.create({ data: createUserInput });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// update(id: string, userUpdateInput: Prisma.UserUpdateInput) {
|
||||||
|
// return this.prismaService.user.update({
|
||||||
|
// where: { id },
|
||||||
|
// data: userUpdateInput,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// remove(id: string) {
|
||||||
|
// return `This action removes a #${id} user`;
|
||||||
|
// }
|
||||||
|
}
|
||||||
24
backend/test/app.e2e-spec.ts
Normal file
24
backend/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
backend/test/jest-e2e.json
Normal file
9
backend/test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
backend/tsconfig.build.json
Normal file
4
backend/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
21
backend/tsconfig.json
Normal file
21
backend/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
5261
backend/yarn.lock
Normal file
5261
backend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@
|
|||||||
"@solidjs/router": "^0.4.3",
|
"@solidjs/router": "^0.4.3",
|
||||||
"@urql/core": "^3.0.3",
|
"@urql/core": "^3.0.3",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"solid-js": "^1.5.1"
|
"solid-js": "^1.5.1",
|
||||||
|
"supertokens-web-js": "^0.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
26
frontend/src/App.tsx
Normal file
26
frontend/src/App.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Component } from "solid-js";
|
||||||
|
import AppRouter from "./routes";
|
||||||
|
|
||||||
|
import SuperTokens from "supertokens-web-js";
|
||||||
|
import EmailPass from "supertokens-web-js/recipe/emailpassword";
|
||||||
|
import Session from "supertokens-web-js/recipe/session";
|
||||||
|
import AuthProvider from "./context/AuthContext";
|
||||||
|
|
||||||
|
SuperTokens.init({
|
||||||
|
appInfo: {
|
||||||
|
apiDomain: "http://localhost:3300",
|
||||||
|
apiBasePath: "/auth/api",
|
||||||
|
appName: "Fluxem",
|
||||||
|
},
|
||||||
|
recipeList: [EmailPass.init(), Session.init()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const App: Component = () => {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<AppRouter />
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
9
frontend/src/components/AuthLoader.tsx
Normal file
9
frontend/src/components/AuthLoader.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const AuthLoader = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Auth Loading...</h2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthLoader;
|
||||||
13
frontend/src/components/TestComponent.tsx
Normal file
13
frontend/src/components/TestComponent.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
interface TestComponentProps {
|
||||||
|
// add props here
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestComponent(props: TestComponentProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>TestComponent</h2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TestComponent;
|
||||||
69
frontend/src/context/AuthContext.tsx
Normal file
69
frontend/src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
import { createContext, onMount, Show, useContext } from "solid-js";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
import { UserType } from "supertokens-web-js/recipe/emailpassword";
|
||||||
|
import AuthLoader from "../components/AuthLoader";
|
||||||
|
import { currentUser } from "../services/auth.service";
|
||||||
|
|
||||||
|
const AuthStateContext = createContext();
|
||||||
|
const AuthDispatchContext = createContext<any>();
|
||||||
|
|
||||||
|
interface InitState {
|
||||||
|
isLoading: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
currentUser: UserType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: InitState = {
|
||||||
|
isLoading: true,
|
||||||
|
isAuthenticated: false,
|
||||||
|
currentUser: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthProvider = (props: any) => {
|
||||||
|
const [store, setStore] = createStore(initialState);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const loadCurrentUser = async () => {
|
||||||
|
const user = await currentUser();
|
||||||
|
if (user) {
|
||||||
|
setStore("isAuthenticated", true);
|
||||||
|
setStore("currentUser", user);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await loadCurrentUser();
|
||||||
|
setStore("isLoading", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const setCurrentUser = (user?: UserType) => {
|
||||||
|
if (user) {
|
||||||
|
setStore("isAuthenticated", true);
|
||||||
|
setStore("currentUser", user);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const removeCurrentUser = () => {
|
||||||
|
setStore("isAuthenticated", false);
|
||||||
|
setStore("currentUser", null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthStateContext.Provider value={store}>
|
||||||
|
<AuthDispatchContext.Provider
|
||||||
|
value={{
|
||||||
|
setCurrentUser,
|
||||||
|
removeCurrentUser,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={!store.isLoading} fallback={<AuthLoader />}>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
</AuthDispatchContext.Provider>
|
||||||
|
</AuthStateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthProvider;
|
||||||
|
export const useAuthState = () => useContext(AuthStateContext);
|
||||||
|
export const useAuthDispatch = () => useContext(AuthDispatchContext);
|
||||||
61
frontend/src/hooks/auth/login.hook.ts
Normal file
61
frontend/src/hooks/auth/login.hook.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
|
import { useAuthDispatch } from "../../context/AuthContext";
|
||||||
|
import {
|
||||||
|
delUserFromLocalStorage,
|
||||||
|
loginService,
|
||||||
|
logoutService,
|
||||||
|
setUserInLocalStorage,
|
||||||
|
} from "../../services/auth.service";
|
||||||
|
|
||||||
|
const useLogin = () => {
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
const [form, setForm] = createStore({
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setCurrentUser } = useAuthDispatch();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleInput = (ev: any) => {
|
||||||
|
setForm([ev.currentTarget.name], ev.currentTarget.value);
|
||||||
|
};
|
||||||
|
const handleLogin = async (ev: any) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const loginData = await loginService({
|
||||||
|
formFields: [
|
||||||
|
{ id: "email", value: form.email },
|
||||||
|
{ id: "password", value: form.password },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (loginData.status === "OK") {
|
||||||
|
setCurrentUser(loginData.user);
|
||||||
|
setUserInLocalStorage(loginData.user);
|
||||||
|
navigate("/", { replace: true });
|
||||||
|
}
|
||||||
|
if (loginData.status === "FIELD_ERROR") {
|
||||||
|
// TODO: Handle error in UI
|
||||||
|
console.log("FIELD_ERROR", loginData);
|
||||||
|
}
|
||||||
|
if (loginData.status === "WRONG_CREDENTIALS_ERROR") {
|
||||||
|
// TODO: Handle error in UI
|
||||||
|
console.log("WRONG_CREDENTIALS_ERROR", loginData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//TODO: Handle error in UI
|
||||||
|
console.error("ERRRRRRRRRRr", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleInput, loading, handleLogin, form };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useLogin;
|
||||||
28
frontend/src/hooks/auth/logout.hook.ts
Normal file
28
frontend/src/hooks/auth/logout.hook.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { createSignal } from "solid-js";
|
||||||
|
import { useAuthDispatch } from "../../context/AuthContext";
|
||||||
|
import {
|
||||||
|
delUserFromLocalStorage,
|
||||||
|
logoutService,
|
||||||
|
} from "../../services/auth.service";
|
||||||
|
|
||||||
|
const useLogout = () => {
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
const { removeCurrentUser } = useAuthDispatch();
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await logoutService();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
removeCurrentUser();
|
||||||
|
delUserFromLocalStorage();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleLogout, loading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useLogout;
|
||||||
34
frontend/src/hooks/auth/register.hook.ts
Normal file
34
frontend/src/hooks/auth/register.hook.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { createSignal } from "solid-js";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
import EmailPassRecipe from "supertokens-web-js/recipe/emailpassword";
|
||||||
|
|
||||||
|
const useRegister = () => {
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
const [form, setForm] = createStore({
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
const handleInput = (ev: any) => {
|
||||||
|
setForm([ev.currentTarget.name], ev.currentTarget.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegister = async (ev: any) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const login = await EmailPassRecipe.signUp({
|
||||||
|
formFields: [
|
||||||
|
{ id: "email", value: form.email },
|
||||||
|
{ id: "password", value: form.password },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { handleInput, loading, handleRegister, form };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useRegister;
|
||||||
@@ -4,7 +4,6 @@ import { render } from "solid-js/web";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { Router } from "@solidjs/router";
|
import { Router } from "@solidjs/router";
|
||||||
|
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<Router>
|
<Router>
|
||||||
@@ -5,7 +5,7 @@ const Home = lazy(() => import("./views/home/Home"));
|
|||||||
const AuthLayout = lazy(() => import("./views/auth/AuthLayout"));
|
const AuthLayout = lazy(() => import("./views/auth/AuthLayout"));
|
||||||
const Login = lazy(() => import("./views/auth/Login"));
|
const Login = lazy(() => import("./views/auth/Login"));
|
||||||
const Register = lazy(() => import("./views/auth/Register"));
|
const Register = lazy(() => import("./views/auth/Register"));
|
||||||
|
const VerifyEmail = lazy(() => import("./views/auth/VerifyEmail"));
|
||||||
const AppRouter: Component = () => {
|
const AppRouter: Component = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -13,6 +13,7 @@ const AppRouter: Component = () => {
|
|||||||
<Route path="/auth" element={<AuthLayout />}>
|
<Route path="/auth" element={<AuthLayout />}>
|
||||||
<Route path="/login" element={<Login />}></Route>
|
<Route path="/login" element={<Login />}></Route>
|
||||||
<Route path="/register" element={<Register />}></Route>
|
<Route path="/register" element={<Register />}></Route>
|
||||||
|
<Route path="/verify-email" element={<VerifyEmail />}></Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
52
frontend/src/routes/views/auth/Login.tsx
Normal file
52
frontend/src/routes/views/auth/Login.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
import { Component, Show } from "solid-js";
|
||||||
|
import { useAuthState } from "../../../context/AuthContext";
|
||||||
|
import useLogin from "../../../hooks/auth/login.hook";
|
||||||
|
const Login: Component = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { handleLogin, handleInput, loading, form } = useLogin();
|
||||||
|
const authState: any = useAuthState();
|
||||||
|
if (authState.isAuthenticated) {
|
||||||
|
navigate("/");
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onsubmit={handleLogin}>
|
||||||
|
<div>
|
||||||
|
<label for="email">Username or Email</label>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="Email/Username"
|
||||||
|
value={form.email}
|
||||||
|
onInput={handleInput}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={form.password}
|
||||||
|
onInput={handleInput}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<button disabled={loading()} type="submit">
|
||||||
|
<Show when={!loading()} fallback="Logging in...">
|
||||||
|
Log In
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default Login;
|
||||||
18
frontend/src/routes/views/auth/VerifyEmail.tsx
Normal file
18
frontend/src/routes/views/auth/VerifyEmail.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import EmailPassRecipe from "supertokens-web-js/recipe/emailpassword";
|
||||||
|
|
||||||
|
const VerifyEmail = () => {
|
||||||
|
let text = "Verifying email";
|
||||||
|
const verifyEmail = EmailPassRecipe.verifyEmail();
|
||||||
|
verifyEmail.then((data) => {
|
||||||
|
console.log(data);
|
||||||
|
text = "Email verified successfully";
|
||||||
|
});
|
||||||
|
// console.log(verifyEmail);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>{text}</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerifyEmail;
|
||||||
31
frontend/src/routes/views/home/Home.tsx
Normal file
31
frontend/src/routes/views/home/Home.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Component, Show } from "solid-js";
|
||||||
|
import { useAuthState } from "../../../context/AuthContext";
|
||||||
|
import useLogin from "../../../hooks/auth/login.hook";
|
||||||
|
import useLogout from "../../../hooks/auth/logout.hook";
|
||||||
|
|
||||||
|
const Home: Component = () => {
|
||||||
|
const authState: any = useAuthState();
|
||||||
|
const { handleLogout, loading } = useLogout();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Home</p>
|
||||||
|
|
||||||
|
<Show when={authState?.isAuthenticated}>
|
||||||
|
<div>
|
||||||
|
<p>Authenticated</p>
|
||||||
|
<button onclick={handleLogout} disabled={loading()}>
|
||||||
|
<Show when={!loading()} fallback="Signing out...">
|
||||||
|
Sign Out
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
<p>{JSON.stringify(authState)}</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={!authState?.isAuthenticated}>
|
||||||
|
<a href="/auth/login">Login</a>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
61
frontend/src/services/auth.service.ts
Normal file
61
frontend/src/services/auth.service.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import EmailPassRecipe, {
|
||||||
|
UserType,
|
||||||
|
} from "supertokens-web-js/recipe/emailpassword";
|
||||||
|
import Session from "supertokens-web-js/recipe/session";
|
||||||
|
|
||||||
|
const curentUserKey = "current_user";
|
||||||
|
|
||||||
|
export interface AuthData {
|
||||||
|
formFields: FormFields[];
|
||||||
|
}
|
||||||
|
interface FormFields {
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUser = async () => {
|
||||||
|
// This method might produce bugs. Localstorage may not be the same as logged in user
|
||||||
|
// TODO: endpoing in backend (getCurrentUser)
|
||||||
|
if (
|
||||||
|
localStorage.getItem("current_user") &&
|
||||||
|
(await Session.doesSessionExist())
|
||||||
|
) {
|
||||||
|
let sessionUserId = await Session.getUserId();
|
||||||
|
const user = getUserFromLocaStorage();
|
||||||
|
if (user) {
|
||||||
|
if (sessionUserId === user.id) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginService = async (loginData: AuthData) => {
|
||||||
|
return await EmailPassRecipe.signIn(loginData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoutService = async () => {
|
||||||
|
await EmailPassRecipe.signOut();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUserInLocalStorage = (user: UserType) => {
|
||||||
|
localStorage.setItem(curentUserKey, JSON.stringify(user));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserFromLocaStorage = () => {
|
||||||
|
const user: UserType = JSON.parse(localStorage.getItem(curentUserKey)!);
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const delUserFromLocalStorage = () => {
|
||||||
|
localStorage.removeItem(curentUserKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
loginService,
|
||||||
|
logoutService,
|
||||||
|
currentUser,
|
||||||
|
setUserInLocalStorage,
|
||||||
|
delUserFromLocalStorage,
|
||||||
|
};
|
||||||
@@ -1297,6 +1297,13 @@ braces@^3.0.2, braces@~3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
|
browser-tabs-lock@^1.2.14:
|
||||||
|
version "1.2.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/browser-tabs-lock/-/browser-tabs-lock-1.2.15.tgz#d5012e652e2a0cb4eba471b0a2300c2fa5d92788"
|
||||||
|
integrity sha512-J8K9vdivK0Di+b8SBdE7EZxDr88TnATing7XoLw6+nFkXMQ6sVBh92K3NQvZlZU91AIkFRi0w3sztk5Z+vsswA==
|
||||||
|
dependencies:
|
||||||
|
lodash ">=4.17.21"
|
||||||
|
|
||||||
browserslist@^4.20.2:
|
browserslist@^4.20.2:
|
||||||
version "4.21.3"
|
version "4.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
|
||||||
@@ -2465,7 +2472,7 @@ lodash.once@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||||
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
|
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
|
||||||
|
|
||||||
lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.0:
|
lodash@>=4.17.21, lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.0:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
@@ -3129,6 +3136,27 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^5.0.1"
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
|
supertokens-js-override@0.0.4, supertokens-js-override@^0.0.4:
|
||||||
|
version "0.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/supertokens-js-override/-/supertokens-js-override-0.0.4.tgz#9af583fbc5e1f0195dbb358c4fcf75f44c76dc09"
|
||||||
|
integrity sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==
|
||||||
|
|
||||||
|
supertokens-web-js@^0.1.6:
|
||||||
|
version "0.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/supertokens-web-js/-/supertokens-web-js-0.1.6.tgz#81165f9f7604518db05088a8a527c756b3b48178"
|
||||||
|
integrity sha512-Cyu97r6tRJc4ryiKhOqYlqYQ+1XB5x5rJfMudg0kgANu7bvNMo39mI4KAb4eLaXQfXAPQ6Y/As+uqUVP7gbKDw==
|
||||||
|
dependencies:
|
||||||
|
supertokens-js-override "0.0.4"
|
||||||
|
supertokens-website "^13.0.2"
|
||||||
|
|
||||||
|
supertokens-website@^13.0.2:
|
||||||
|
version "13.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/supertokens-website/-/supertokens-website-13.0.2.tgz#c1eba0d1745ee2d7c019fa88eb9d7029309fc6e9"
|
||||||
|
integrity sha512-WVCHky05ndJhPXW2khAWy3CBlWn1ZwRF32TPTiGgYLrpQYO9q5doHJi9z2H1OiSmOEFJDEKWtaPW9HxAHABo1Q==
|
||||||
|
dependencies:
|
||||||
|
browser-tabs-lock "^1.2.14"
|
||||||
|
supertokens-js-override "^0.0.4"
|
||||||
|
|
||||||
supports-color@^5.3.0:
|
supports-color@^5.3.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { Component } from "solid-js";
|
|
||||||
import AppRouter from "./routes";
|
|
||||||
|
|
||||||
const App: Component = () => {
|
|
||||||
return <AppRouter />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { createSignal } from "solid-js";
|
|
||||||
import { createStore } from "solid-js/store";
|
|
||||||
|
|
||||||
const useLogin = () => {
|
|
||||||
const [loading, setLoading] = createSignal(false);
|
|
||||||
const [form, setForm] = createStore({
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
});
|
|
||||||
const handleInput = (ev: any) => {
|
|
||||||
setForm([ev.currentTarget.name], ev.currentTarget.value);
|
|
||||||
};
|
|
||||||
const handleLogin = (ev: any) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
return { handleInput, loading, handleLogin, form };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useLogin;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Component, Show } from "solid-js";
|
|
||||||
import useLogin from "../../../hooks/auth/login.hook";
|
|
||||||
const { handleLogin, handleInput, loading, form } = useLogin();
|
|
||||||
const Login: Component = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onsubmit={handleLogin}>
|
|
||||||
<div>
|
|
||||||
{/* <label for="name">Username or Email</label> */}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="username"
|
|
||||||
placeholder="Email/Username"
|
|
||||||
value={form.username}
|
|
||||||
onInput={handleInput}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{/* <label for="name">Password</label> */}
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
placeholder="Password"
|
|
||||||
value={form.password}
|
|
||||||
onInput={handleInput}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button disabled={loading()} type="submit">
|
|
||||||
<Show when={!loading()} fallback="Logging in...">
|
|
||||||
Log In
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { Component } from "solid-js";
|
|
||||||
|
|
||||||
const Home: Component = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Home</h2>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Home;
|
|
||||||
Reference in New Issue
Block a user