diff options
12 files changed, 338 insertions, 33 deletions
diff --git a/Backend/Api/Api/Api.csproj b/Backend/Api/Api/Api.csproj index 80898fd..24c41b7 100644 --- a/Backend/Api/Api/Api.csproj +++ b/Backend/Api/Api/Api.csproj @@ -13,6 +13,7 @@ <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.10" /> <PackageReference Include="MongoDB.Driver" Version="2.18.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> + <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.5" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="MailKit" Version="3.4.2" /> diff --git a/Backend/Api/Api/Controllers/PostController.cs b/Backend/Api/Api/Controllers/PostController.cs index a61ee2e..27823bc 100644 --- a/Backend/Api/Api/Controllers/PostController.cs +++ b/Backend/Api/Api/Controllers/PostController.cs @@ -14,10 +14,12 @@ namespace Api.Controllers { private readonly IPostService _postService; private readonly IFileService _fileService; - public PostController(IPostService postService, IFileService fileService) + private readonly IUserService _userService; + public PostController(IPostService postService, IFileService fileService,IUserService userService) { _postService = postService; _fileService = fileService; + _userService = userService; } [HttpPost("add")] @@ -46,7 +48,8 @@ namespace Api.Controllers [Authorize(Roles = "User")] public async Task<ActionResult<PostSend>> getPostByid(string id) { - var res = await _postService.getPostById(id); + var userid = await _userService.UserIdFromJwt(); + var res = await _postService.getPostById(id,userid); if (res != null) { return Ok(res); @@ -62,10 +65,56 @@ namespace Api.Controllers if (f == null || !System.IO.File.Exists(f.path)) return BadRequest("Slika ne postoji"); return File(System.IO.File.ReadAllBytes(f.path), "image/*", Path.GetFileName(f.path)); + } + [HttpPost("posts/{id}/addrating")] + [Authorize(Roles = "User")] + public async Task<ActionResult> addRating([FromBody] RatingReceive rating,string id) + { + var userid = await _userService.UserIdFromJwt(); + if (await _postService.AddOrReplaceRating(rating, userid)) + return Ok(); + return BadRequest(); + } + [HttpDelete("posts/{id}/removerating")] + [Authorize(Roles = "User")] + public async Task<ActionResult> removeRating(string id) + { + var userid = await _userService.UserIdFromJwt(); + if (await _postService.RemoveRating(id,userid)) + return Ok(); + return BadRequest(); } + [HttpPost("posts/{id}/addcomment")] + [Authorize(Roles = "User")] + public async Task<ActionResult> addComment([FromBody] CommentReceive cmnt,string id) + { + var userid = await _userService.UserIdFromJwt(); + if (await _postService.AddComment(cmnt,userid,id)) + return Ok(); + return BadRequest(); + } + + [HttpGet("posts/{id}/listcomments")] + [Authorize(Roles = "User")] + public async Task<ActionResult<List<CommentSend>>> listComments(string id) + { + var ret = await _postService.ListComments(id); + if(ret != null) + return Ok(ret); + return BadRequest(); + } + [HttpDelete("posts/{id}/removecomment/{cmntid}")] + [Authorize(Roles = "User")] + public async Task<ActionResult> removeRating(string id,string cmntid) + { + var userid = await _userService.UserIdFromJwt(); + if (await _postService.DeleteComments(id,cmntid,userid)) + return Ok(); + return BadRequest(); + } } } diff --git a/Backend/Api/Api/Interfaces/IPostService.cs b/Backend/Api/Api/Interfaces/IPostService.cs index 29a824a..daeee92 100644 --- a/Backend/Api/Api/Interfaces/IPostService.cs +++ b/Backend/Api/Api/Interfaces/IPostService.cs @@ -6,7 +6,14 @@ namespace Api.Interfaces { Task<PostSend> addPost(PostReceive post); Task<List<PostSend>> getAllPosts(); - Task<PostSend> getPostById(string id); + Task<PostSend> getPostById(string id,string userid); Task<PostSend> postToPostSend(Post post); + Task<Boolean> AddOrReplaceRating(RatingReceive rating, string userid); + Task<Boolean> RemoveRating(string postid, string userid); + Task<Boolean> AddComment(CommentReceive cmnt, string userid, string postid); + Task<List<CommentSend>> ListComments(string postid); + Task<List<CommentSend>> CascadeComments(string parentid, Post p); + Task<Boolean> DeleteComments(string postid, string cmntid,string userid); + Task CascadeDeleteComments(string cmntid, Post p); } }
\ No newline at end of file diff --git a/Backend/Api/Api/Models/Post.cs b/Backend/Api/Api/Models/Post.cs index ee84e0f..c832d23 100644 --- a/Backend/Api/Api/Models/Post.cs +++ b/Backend/Api/Api/Models/Post.cs @@ -20,9 +20,7 @@ namespace Api.Models } public class PostReceive { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - public string _id { get; set; } + public string? _id { get; set; } public string locationId { get; set; } public string description { get; set; } public List<IFormFile> images { get; set; } @@ -31,15 +29,13 @@ namespace Api.Models } public class PostSend { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] public string _id { get; set; } public string ownerId { get; set; } public Location location { get; set; } public string description { get; set; } public int views { get; set; } - public float ratings { get; set; } - public List<Comment> comments { get; set; } + public double ratings { get; set; } + public List<CommentSend> comments { get; set; } public List<File> images { get; set; } } public class Rating @@ -49,9 +45,33 @@ namespace Api.Models } public class Comment { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string _id { get; set; } + public string userId { get; set; } + public string comment { get; set; } + public string parentId { get; set; } + public DateTime timestamp { get; set; } + } + + public class RatingReceive + { + public int rating { get; set; } + public string postId { get; set; } + } + public class CommentSend + { + public string _id { get; set; } public string userId { get; set; } public string comment { get; set; } - public Comment parent { get; set; } + public string? parentId { get; set; } public DateTime timestamp { get; set; } + public string username { get; set; } + public List<CommentSend> replies { get; set; } + } + public class CommentReceive + { + public string comment { get; set; } + public string parentId { get; set; } } } diff --git a/Backend/Api/Api/Program.cs b/Backend/Api/Api/Program.cs index 16b0241..7d6b03e 100644 --- a/Backend/Api/Api/Program.cs +++ b/Backend/Api/Api/Program.cs @@ -4,7 +4,9 @@ using Api.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using MongoDB.Driver; +using Swashbuckle.AspNetCore.Filters; using System.Text; var builder = WebApplication.CreateBuilder(args); @@ -50,7 +52,17 @@ builder.Services.AddAuthentication( builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => { + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Description = "Standard Authorization header using the Bearer scheme (\"bearer {token}\")", + In = ParameterLocation.Header, + Name = "Authorization", + Type = SecuritySchemeType.ApiKey + }); + + options.OperationFilter<SecurityRequirementsOperationFilter>(); +}); builder.Services.AddCors(options => { diff --git a/Backend/Api/Api/Services/PostService.cs b/Backend/Api/Api/Services/PostService.cs index e9a56d2..78167bd 100644 --- a/Backend/Api/Api/Services/PostService.cs +++ b/Backend/Api/Api/Services/PostService.cs @@ -2,6 +2,8 @@ using Api.Interfaces; using Api.Models; using MongoDB.Driver; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson; namespace Api.Services { @@ -12,10 +14,12 @@ namespace Api.Services private readonly IHttpContextAccessor _httpContext; private readonly IFileService _fileService; private readonly ILocationService _locationService; + private readonly IMongoCollection<User> _users; public PostService(IDatabaseConnection settings, IMongoClient mongoClient, IHttpContextAccessor httpContext, IFileService fileService,ILocationService locationService) { var database = mongoClient.GetDatabase(settings.DatabaseName); _posts = database.GetCollection<Post>(settings.PostCollectionName); + _users = database.GetCollection<User>(settings.UserCollectionName); _httpContext = httpContext; _fileService = fileService; _locationService = locationService; @@ -25,7 +29,7 @@ namespace Api.Services { Post p = new Post(); p._id = ""; - p.ownerId = _httpContext.HttpContext.User.FindFirstValue("id").ToString(); + p.ownerId = _httpContext.HttpContext.User.FindFirstValue("id"); p.locationId = post.locationId; p.description = post.description; @@ -77,8 +81,16 @@ namespace Api.Services p.description = post.description; p.location = await _locationService.getById(post.locationId); p.images = post.images; - p.views = 1;//Default values todo - p.ratings = 1; + p.views = post.views.Count(); + if (post.ratings.Count() > 0) + { + List<int> ratings = new List<int>(); + foreach (var r in post.ratings) + ratings.Add(r.rating); + p.ratings = ratings.Average(); + } + else + p.ratings = 0; p.comments = null; @@ -96,12 +108,172 @@ namespace Api.Services return temp; } - public async Task<PostSend> getPostById(string id) + public async Task<PostSend> getPostById(string id,string userid) { Post p = await _posts.Find(post => post._id == id).FirstOrDefaultAsync(); + if (p != null) + { + if (!p.views.Any(x => x == userid)) + { + p.views.Add(userid); + await _posts.ReplaceOneAsync(x => x._id == id, p); + } + } return await postToPostSend(p); + } + + public async Task<Boolean> AddOrReplaceRating(RatingReceive rating,string userid) + { + Post p = await _posts.Find(post => post._id == rating.postId).FirstOrDefaultAsync(); + if (p != null) + { + if (p.ownerId == userid) + return false; + if(!p.ratings.Any(x => x.userId == userid)) + { + Rating r = new Rating(); + r.rating = rating.rating; + r.userId = userid; + p.ratings.Add(r); + await _posts.ReplaceOneAsync(x => x._id == p._id, p); + } + else + { + var r = p.ratings.Find(x => x.userId == userid); + p.ratings.Remove(r); + r.rating = rating.rating; + p.ratings.Add(r); + await _posts.ReplaceOneAsync(x => x._id == p._id, p); + } + return true; + } + return false; + } + public async Task<Boolean> RemoveRating(string postid, string userid) + { + Post p = await _posts.Find(post => post._id == postid).FirstOrDefaultAsync(); + if (p != null) + { + if (p.ratings.Any(x => x.userId == userid)) + { + var r = p.ratings.Find(x => x.userId == userid); + p.ratings.Remove(r); + await _posts.ReplaceOneAsync(x => x._id == postid, p); + return true; + } + } + return false; + } + public async Task<Boolean> AddComment(CommentReceive cmnt,string userid,string postid) + { + Post p = await _posts.Find(post => post._id == postid).FirstOrDefaultAsync(); + if (p != null) + { + Comment c= new Comment(); + c.parentId = cmnt.parentId; + c.userId = userid; + c.comment = cmnt.comment; + c.timestamp = DateTime.Now.ToUniversalTime(); + c._id = ObjectId.GenerateNewId().ToString(); + p.comments.Add(c); + await _posts.ReplaceOneAsync(x => x._id == postid, p); + return true; + } + return false; + } + public async Task<List<CommentSend>> ListComments(string postid) + { + Post p = await _posts.Find(post => post._id == postid).FirstOrDefaultAsync(); + if (p != null) + { + List<Comment> lista = new List<Comment>(); + lista = p.comments.FindAll(x => x.parentId == null || x.parentId == ""); + if (lista.Count() > 0) + { + List<CommentSend> tosend = new List<CommentSend>(); + foreach(var comment in lista) + { + CommentSend c = new CommentSend(); + c.userId = comment.userId; + c._id = comment._id; + c.parentId = comment.parentId; + c.comment = comment.comment; + c.timestamp = comment.timestamp; + + var user = await _users.Find(x => x._id == comment.userId).FirstOrDefaultAsync(); + if (user != null) + c.username = user.username; + else c.username = "Deleted user"; + + c.replies = await CascadeComments(comment._id, p); + + tosend.Add(c); + } + return tosend; + } + } + return null; + } + public async Task<List<CommentSend>> CascadeComments(string parentid,Post p) + { + List<Comment> lista = new List<Comment>(); + lista = p.comments.FindAll(x => x.parentId == parentid); + if (lista.Count()>0) + { + List<CommentSend> replies = new List<CommentSend>(); + foreach (var comment in lista) + { + CommentSend c = new CommentSend(); + c.userId = comment.userId; + c._id = comment._id; + c.parentId = comment.parentId; + c.comment = comment.comment; + c.timestamp = comment.timestamp; + + var user= await _users.Find(x => x._id == comment.userId).FirstOrDefaultAsync(); + if (user != null) + c.username = user.username; + else c.username = "Deleted user"; + + c.replies = await CascadeComments(comment._id, p); + + replies.Add(c); + } + return replies; + } + return null; + } + public async Task<Boolean> DeleteComments(string postid,string cmntid,string userid) + { + Post p = await _posts.Find(post => post._id == postid).FirstOrDefaultAsync(); + if (p != null) + { + var com = p.comments.Find(x => x._id == cmntid); + if (com != null && com.userId == userid) + { + var comment = p.comments.Find(x => x._id == cmntid); + p.comments.Remove(comment); + await _posts.ReplaceOneAsync(x => x._id == p._id, p); + await CascadeDeleteComments(cmntid, p); + return true; + } + } + return false; + } + public async Task CascadeDeleteComments(string cmntid,Post p) + { + List<Comment> lista = new List<Comment>(); + lista = p.comments.FindAll(x => x.parentId == cmntid); + if (lista.Count() > 0) + { + foreach (var comment in lista) + { + p.comments.Remove(comment); + await _posts.ReplaceOneAsync(x => x._id == p._id, p); + await CascadeDeleteComments(comment._id, p); + } + } } - //(TODO) ADD Delete and update } } diff --git a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Activities/ActivitySinglePost.kt b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Activities/ActivitySinglePost.kt index be4a73d..6a5dfe3 100644 --- a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Activities/ActivitySinglePost.kt +++ b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Activities/ActivitySinglePost.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.example.brzodolokacije.Adapters.PostImageAdapter import com.example.brzodolokacije.Models.PostImage @@ -46,9 +47,9 @@ class ActivitySinglePost : AppCompatActivity() { binding.apply { tvTitle.text= post.location.name tvTitle.invalidate() - tvLocationType.text=post.location.type.name + tvLocationType.text="TODO" tvLocationType.invalidate() - tvLocationParent.text=post.location.country + tvLocationParent.text="TODO" tvLocationParent.invalidate() tvRating.text=post.ratings.toString() tvRating.invalidate() diff --git a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Adapters/ShowPostsAdapter.kt b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Adapters/ShowPostsAdapter.kt index 8e6093e..134b665 100644 --- a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Adapters/ShowPostsAdapter.kt +++ b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Adapters/ShowPostsAdapter.kt @@ -1,24 +1,40 @@ package com.example.brzodolokacije.Adapters import android.app.Activity -import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory +import android.os.AsyncTask import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import com.example.brzodolokacije.Activities.ActivitySinglePost +import com.example.brzodolokacije.Interfaces.IBackendApi +import com.example.brzodolokacije.Models.LocationType import com.example.brzodolokacije.Models.PostPreview +import com.example.brzodolokacije.Services.RetrofitHelper +import com.example.brzodolokacije.Services.SharedPreferencesHelper import com.example.brzodolokacije.databinding.PostPreviewBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Response + class ShowPostsAdapter (val activity:Activity,val items : MutableList<PostPreview>) : RecyclerView.Adapter<ShowPostsAdapter.ViewHolder>() { + private lateinit var token: String + private lateinit var imageApi: IBackendApi + //constructer has one argument - list of objects that need to be displayed //it is bound to xml of single item private lateinit var binding: PostPreviewBinding override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = LayoutInflater.from(parent.context) + imageApi= RetrofitHelper.getInstance() + token= SharedPreferencesHelper.getValue("jwt", activity).toString() binding = PostPreviewBinding.inflate(inflater, parent, false) return ViewHolder(binding) } @@ -30,6 +46,7 @@ class ShowPostsAdapter (val activity:Activity,val items : MutableList<PostPrevie //Toast.makeText(activity,item._id,Toast.LENGTH_LONG).show() val intent:Intent = Intent(activity,ActivitySinglePost::class.java) var b=Bundle() + items[position].location.type=LocationType.ADA b.putParcelable("selectedPost", items[position]) intent.putExtras(b) activity.startActivity(intent) @@ -43,9 +60,34 @@ class ShowPostsAdapter (val activity:Activity,val items : MutableList<PostPrevie binding.apply { tvTitle.text = item.location.name tvLocationParent.text = item.location.country - tvLocationType.text = item.location.type.toString() + tvLocationType.text = "TODO" + + val request=imageApi.getImage("Bearer "+token,item.images[0]._id) + + request.enqueue(object : retrofit2.Callback<ResponseBody?> { + override fun onResponse(call: Call<ResponseBody?>, response: Response<ResponseBody?>) { + if (response.isSuccessful) { + val image: ResponseBody = response.body()!! + binding.locationImage.setImageBitmap(BitmapFactory.decodeStream(image.byteStream())) + Toast.makeText( + activity, "prosao zahtev", Toast.LENGTH_LONG + ).show() + } else { + if (response.errorBody() != null) + Toast.makeText( + activity, + response.errorBody()!!.string(), + Toast.LENGTH_LONG + ).show(); + } + } - itemView.isClickable = true + override fun onFailure(call: Call<ResponseBody?>, t: Throwable) { + Toast.makeText( + activity, t.toString(), Toast.LENGTH_LONG + ).show(); + } + }) } } diff --git a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Fragments/FragmentShowPosts.kt b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Fragments/FragmentShowPosts.kt index e9b4c08..9a0eedc 100644 --- a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Fragments/FragmentShowPosts.kt +++ b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Fragments/FragmentShowPosts.kt @@ -3,6 +3,7 @@ package com.example.brzodolokacije.Fragments import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View @@ -43,8 +44,9 @@ class FragmentShowPosts : Fragment() { request.enqueue(object : retrofit2.Callback<MutableList<PostPreview>?> { override fun onResponse(call: Call<MutableList<PostPreview>?>, response: Response<MutableList<PostPreview>?>) { if(response.isSuccessful){ - //posts=response.body()!! - //recyclerView?.adapter=ShowPostsAdapter(requireActivity(),posts) + posts=response.body()!! + Log.d("main",posts[0].toString()) + recyclerView?.adapter=ShowPostsAdapter(requireActivity(),posts) Toast.makeText( activity, "prosao zahtev", Toast.LENGTH_LONG ).show() diff --git a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Interfaces/IBackendApi.kt b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Interfaces/IBackendApi.kt index bcb6e13..49dda46 100644 --- a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Interfaces/IBackendApi.kt +++ b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Interfaces/IBackendApi.kt @@ -7,10 +7,7 @@ import com.example.brzodolokacije.Models.Auth.ResetPass import com.example.brzodolokacije.Models.PostPreview import okhttp3.ResponseBody import retrofit2.Call -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.POST +import retrofit2.http.* interface IBackendApi { @POST("/api/auth/login") @@ -25,6 +22,9 @@ interface IBackendApi { fun resetpass(@Body obj:ResetPass):Call<ResponseBody> @GET("/api/post") fun getPosts(@Header("Authorization") authHeader:String):Call<MutableList<PostPreview>> + @Streaming + @GET("/api/post/image/{id}") + fun getImage(@Header("Authorization") authHeader:String,@Path("id") obj:String):Call<ResponseBody> //@POST("putanja") //fun add(@Body obj:Post,@Header("Authorization") authHeader:String):Call<Post> }
\ No newline at end of file diff --git a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Models/Location.kt b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Models/Location.kt index 04bf3a1..c5fe48a 100644 --- a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Models/Location.kt +++ b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Models/Location.kt @@ -1,7 +1,6 @@ package com.example.brzodolokacije.Models import android.os.Parcelable -import com.example.brzodolokacije.Models.LocationType import kotlinx.android.parcel.Parcelize @Parcelize @@ -10,8 +9,8 @@ data class Location ( var name:String, var city:String, var country:String, - var adress:String, + var address:String, var latitude:Double, var longitude:Double, - var type:LocationType - ): Parcelable + var type:LocationType? +): Parcelable diff --git a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Models/Post.kt b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Models/Post.kt index f667fac..9b9afaa 100644 --- a/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Models/Post.kt +++ b/Client/BrzoDoLokacije/app/src/main/java/com/example/brzodolokacije/Models/Post.kt @@ -33,7 +33,7 @@ data class PostPreview ( var description:String, var views:Int, var ratings:Float, - var comments:List<Comment>, + var comments:List<Comment>?, var images:List<PostImage> ):Parcelable |