Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Droplet Deployer

on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ deploy-production ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
deploy:
# The type of runner that the job will run on
runs-on: self-hosted
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v2
- name: Deply and restart web server
run: |
cd server
npm install
npm run start:prod
34 changes: 27 additions & 7 deletions server/api.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,52 @@
import { Post, User } from './types';
import { Post, User,Like,Comment } from './types';

// Post APIs
export interface ListPostsRequest {}
export interface ListPostsResponse {
posts: Post[];
}

export type CreatePostRequest = Pick<Post, 'title' | 'url' | 'userId'>;
export type DeletePostRequest = { postId: string };
export type DeletePostResponse = {};
export type CreatePostRequest = Pick<Post, 'title' | 'url'>;
export interface CreatePostResponse {}

export interface GetPostRequest {}
export type GetPostRequest = {postId:string}
export interface GetPostResponse {
post: Post;
}

// Comment APIs
export type CreateCommentRequest = Pick<Comment,'postId' | 'comment'>;
export interface CreateCommentResponse {}
export type GetCommentsRequest = { postId: string };
export interface GetCommentsResponse {
comments: Comment[];
}
export type DeleteCommentRequest = { commentId: string };
export type DeleteCommentResponse = {};

// Like APIs
export type CreateLikeRequest = Like;
export interface CreateLikeResponse {}
export type GetLikesRequest = { postId: string };
export interface GetLikesResponse {
likes: Number;
}

// User APIs
export type SignUpRequest = Pick<
User,
'email' | 'firstName' | 'lastName' | 'username' | 'password'
>;
export interface SignUpResponse {}
export interface SignUpResponse {
jwt: string
}

export interface SignInRequest {
login: string; // username or email
password: string;
}
export type SignInResponse = Pick<User, 'email' | 'firstName' | 'lastName' | 'username' | 'id'>;

export type SignInResponse =
{ user: Pick<User, 'email' | 'firstName' | 'lastName' | 'username' | 'id'>;
jwt: string;
}
13 changes: 13 additions & 0 deletions server/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import jwt from 'jsonwebtoken';
import { JwtObject } from './types';
import {getJwtSecret} from './env';

export function signJwt(obj: JwtObject): string {
return jwt.sign(obj, getJwtSecret(), {
expiresIn: '7d',
});
}

export function verifyJwt(token: string): JwtObject {
return jwt.verify(token, getJwtSecret()) as JwtObject;
}
2 changes: 2 additions & 0 deletions server/datastore/dao/LikeDao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ import { Like } from '../../types';

export interface LikeDao {
createLike(like: Like): Promise<void>;
getLikes(postId:string): Promise<number>;
isDuplicateLike(like:Like): Promise<boolean>;
}
3 changes: 2 additions & 1 deletion server/datastore/dao/UserDao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { User } from '../../types';

export interface UserDao {
createUser(user: User): Promise<void>;
getUserById(id: string): Promise<User|undefined>;
getUserByEmail(email: string): Promise<User | undefined>;
getUserByUsername(email: string): Promise<User | undefined>;
getUserByUsername(userName: string): Promise<User | undefined>;
}
18 changes: 16 additions & 2 deletions server/datastore/memorydb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ export class InMemoryDatastore implements Datastore {
return Promise.resolve();
}

getUserById(id: string): Promise<User | undefined> {
return Promise.resolve(this.users.find(u => u.id === id));
}

getUserByEmail(email: string): Promise<User | undefined> {
return Promise.resolve(this.users.find(u => u.email === email));
}

getUserByUsername(email: string): Promise<User | undefined> {
return Promise.resolve(this.users.find(u => u.username === email));
getUserByUsername(userName: string): Promise<User | undefined> {
return Promise.resolve(this.users.find(u => u.username === userName));
}

listPosts(): Promise<Post[]> {
Expand Down Expand Up @@ -64,4 +68,14 @@ export class InMemoryDatastore implements Datastore {
this.posts.splice(index, 1);
return Promise.resolve();
}

getLikes(postId: string): Promise<number> {
const likes = this.likes.filter(x => x.postId === postId).length;
return Promise.resolve(likes);
}

isDuplicateLike(like: Like): Promise<boolean> {
const isExists = this.likes.indexOf(like) >= 0;
return Promise.resolve(isExists);
}
}
Binary file modified server/datastore/sql/codersquare.sqlite
Binary file not shown.
60 changes: 47 additions & 13 deletions server/datastore/sql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ export class SqlDataStore implements Datastore {
user.username
);
}

getUserById(id: string): Promise<User | undefined> {
return this.db.get<User>(`SELECT * FROM users WHERE id = ?`, id);
}

getUserByEmail(email: string): Promise<User | undefined> {
return this.db.get<User>(`SELECT * FROM users WHERE email = ?`, email);
}

getUserByUsername(username: string): Promise<User | undefined> {
return this.db.get<User>(`SELECT * FROM users WHERE username = ?`, username);
return this.db.get<User>(`SELECT * FROM users WHERE userName = ?`, username);
}

listPosts(): Promise<Post[]> {
Expand All @@ -59,22 +63,52 @@ export class SqlDataStore implements Datastore {
);
}

getPost(id: string): Promise<Post | undefined> {
throw new Error('Method not implemented.');
async getPost(id: string): Promise<Post | undefined> {
return await this.db.get<Post>('SELECT * FROM posts WHERE postId = ?', id);
}

async deletePost(id: string): Promise<void> {
await this.db.run('Delete FROM posts WHERE id=?', id);
}
deletePost(id: string): Promise<void> {
throw new Error('Method not implemented.');

async createLike(like: Like): Promise<void> {
await this.db.run('INSERT INTO Likes(userId,postId) VALUES(?,?)', like.userId, like.postId);
}

async createComment(comment: Comment): Promise<void> {
await this.db.run(
'INSERT INTO Comments(userId,postId,comment,postedAt) VALUES(?,?,?,?)',
comment.userId,
comment.postId,
comment.comment,
comment.postedAt
);
}
createLike(like: Like): Promise<void> {
throw new Error('Method not implemented.');

async listComments(postId: string): Promise<Comment[]> {
return await this.db.all<Comment[]>('SELECT * FROM comments WHERE postId=?', postId);
}
createComment(comment: Comment): Promise<void> {
throw new Error('Method not implemented.');

async deleteComment(id: string): Promise<void> {
await this.db.run('Delete FROM comments WHERE Id=?', id);
}
listComments(postId: string): Promise<Comment[]> {
throw new Error('Method not implemented.');

async getLikes(postId: string): Promise<number> {
let awaitResult = await this.db.get<number>(
'Select count(*) FROM Likes WHERE postId=?',
postId
);
let val: number = awaitResult === undefined ? 0 : awaitResult;
return val;
}
deleteComment(id: string): Promise<void> {
throw new Error('Method not implemented.');

async isDuplicateLike(like: Like): Promise<boolean> {
let awaitResult = await this.db.get<number>(
'SELECT 1 FROM likes WHERE postId=?,userId=?',
like.postId,
like.userId
);
let val: boolean = awaitResult === undefined ? false : true;
return val;
}
}
2 changes: 2 additions & 0 deletions server/datastore/sql/migrations/001-initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ CREATE TABLE posts (
postedAt INTEGER NOT NULL,
FOREIGN KEY (userId) REFERENCES users (id)
);


17 changes: 17 additions & 0 deletions server/datastore/sql/migrations/002-add-comment-likes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE Table comments (
id VARCHAR PRIMARY KEY,
userId VARCHAR NOT NULL,
postId VARCHAR NOT NULL,
comment VARCHAR NOT NULL,
postedAt INTEGER NOT NULL,
FOREIGN KEY (userId) REFERENCES users (id),
FOREIGN KEY (PostId) REFERENCES posts (id)
);

CREATE Table likes (
userId VARCHAR NOT NULL,
postId VARCHAR NOT NULL,
FOREIGN KEY (userId) REFERENCES users (id),
FOREIGN KEY (postId) REFERENCES posts (id),
PRIMARY KEY (userId, postId)
);
19 changes: 19 additions & 0 deletions server/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Throws on bad tokens
export function getJwtSecret(): string {
const secret = process.env.JWT_SECRET;
if (!secret) {
console.error('Missing JWT secret');
process.exit(1);
}
return secret!;
}


export function getSalt(): string {
const salt = process.env.PASSWORD_SALT;
if (!salt) {
console.error('Missing Password salt');
process.exit(1);
}
return salt!;
}
35 changes: 25 additions & 10 deletions server/handlers/userHandler.ts → server/handlers/authHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import crypto from 'crypto';

import { SignInRequest, SignInResponse, SignUpRequest, SignUpResponse } from '../api';
import { signJwt } from '../auth';
import { db } from '../datastore';
import { ExpressHandler, User } from '../types';

Expand All @@ -11,38 +12,52 @@ export const signInHandler: ExpressHandler<SignInRequest, SignInResponse> = asyn
}

const existing = (await db.getUserByEmail(login)) || (await db.getUserByUsername(login));
if (!existing || existing.password !== password) {
if (!existing || existing.password !== hashPassword(password)) {
return res.sendStatus(403);
}

const jwt = signJwt({ userId: existing.id });

return res.status(200).send({
email: existing.email,
firstName: existing.firstName,
lastName: existing.lastName,
id: existing.id,
username: existing.username,
user: {
email: existing.email,
firstName: existing.firstName,
lastName: existing.lastName,
id: existing.id,
username: existing.username,
},
jwt: jwt,
});
};

export const signUpHandler: ExpressHandler<SignUpRequest, SignUpResponse> = async (req, res) => {
const { email, firstName, lastName, password, username } = req.body;
if (!email || !firstName || !lastName || !username || !password) {
return res.status(400).send('All fields are required');
return res.status(400).send({error:'All fields are required'});
}

const existing = (await db.getUserByEmail(email)) || (await db.getUserByUsername(username));
if (existing) {
return res.status(403).send('User already exists');
return res.status(403).send({error:'User already exists'});
}


const user: User = {
id: crypto.randomUUID(),
email,
firstName,
lastName,
username,
password,
password:hashPassword(password),
};
const jwt = signJwt({userId: user.id});

await db.createUser(user);
return res.sendStatus(200);
return res.status(200).send({
jwt
})
};

function hashPassword(password:string): string{
return crypto.pbkdf2Sync(password, process.env.PASSWORD_SALT!,10,32,'sha512').toString('hex');
}
49 changes: 49 additions & 0 deletions server/handlers/commentHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { CreateCommentRequest, CreateCommentResponse, DeleteCommentRequest, DeleteCommentResponse, GetCommentsRequest, GetCommentsResponse } from '../api';
import { db } from '../datastore';
import { ExpressHandler, Comment } from '../types';
import crypto from 'crypto';

//Create Comment Handler
export const createCommentHandler
:ExpressHandler<CreateCommentRequest,CreateCommentResponse> = async (req,res)=>{

if(!req.body.postId)
return res.status(400).send({error:"No Post Id"});

if(!res.locals.userId)
return res.status(400).send({error:"No User Id"});

if(!req.body.comment)
return res.status(400).send({error:"No Comment"});

const commentForInsertion:Comment = {
id:crypto.randomUUID(),
postedAt: Date.now(),
postId:req.body.postId,
userId:res.locals.userId,
comment:req.body.comment
}
await db.createComment(commentForInsertion);
return res.sendStatus(200);
}

//Delete Comment Handler
export const deleteCommentHandler: ExpressHandler<DeleteCommentRequest, DeleteCommentResponse> = async (
req,
res
) => {
if (!req.body.commentId)
return res.status(404).send({error:"No Comment Id"});
await db.deleteComment(req.body.commentId);
return res.sendStatus(200);
};

//Get Post Comments
export const getCommentsHandler: ExpressHandler<GetCommentsRequest,GetCommentsResponse> = async (
req,
res
) => {
if (!req.body.postId)
return res.status(404).send({error:"No Post Id"});
await db.listComments(req.body.postId);
};
Loading